Repository: squashql/squashql
Pull request #301 (2024-04-05 15:16:32)

Partial measure


Patch:
diff --git a/core/src/main/java/io/squashql/query/AggregatedMeasure.java b/core/src/main/java/io/squashql/query/AggregatedMeasure.java
index ee755db..493aaa3 100644
--- a/core/src/main/java/io/squashql/query/AggregatedMeasure.java
+++ b/core/src/main/java/io/squashql/query/AggregatedMeasure.java
@@ -1,5 +1,6 @@
 package io.squashql.query;
 
+import io.squashql.query.measure.visitor.MeasureVisitor;
 import io.squashql.query.dto.CriteriaDto;
 import lombok.*;
 
@@ -54,4 +55,9 @@ public class AggregatedMeasure implements BasicMeasure {
   public String expression() {
     return this.expression;
   }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/Axis.java b/core/src/main/java/io/squashql/query/Axis.java
new file mode 100644
index 0000000..72edef1
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/Axis.java
@@ -0,0 +1,6 @@
+package io.squashql.query;
+
+public enum Axis {
+  ROW,
+  COLUMN,
+}
diff --git a/core/src/main/java/io/squashql/query/BinaryOperationMeasure.java b/core/src/main/java/io/squashql/query/BinaryOperationMeasure.java
index 3f44bf9..e2e2ebc 100644
--- a/core/src/main/java/io/squashql/query/BinaryOperationMeasure.java
+++ b/core/src/main/java/io/squashql/query/BinaryOperationMeasure.java
@@ -1,5 +1,6 @@
 package io.squashql.query;
 
+import io.squashql.query.measure.visitor.MeasureVisitor;
 import lombok.*;
 
 @ToString
@@ -35,4 +36,8 @@ public class BinaryOperationMeasure implements Measure {
     return this.expression;
   }
 
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/ComparisonMeasureGrandTotal.java b/core/src/main/java/io/squashql/query/ComparisonMeasureGrandTotal.java
index 916f5f1..1b04062 100644
--- a/core/src/main/java/io/squashql/query/ComparisonMeasureGrandTotal.java
+++ b/core/src/main/java/io/squashql/query/ComparisonMeasureGrandTotal.java
@@ -1,5 +1,6 @@
 package io.squashql.query;
 
+import io.squashql.query.measure.visitor.MeasureVisitor;
 import lombok.*;
 
 @ToString
@@ -33,4 +34,9 @@ public class ComparisonMeasureGrandTotal implements Measure {
   public String expression() {
     return this.expression;
   }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/ComparisonMeasureReferencePosition.java b/core/src/main/java/io/squashql/query/ComparisonMeasureReferencePosition.java
index f4c5f40..6545965 100644
--- a/core/src/main/java/io/squashql/query/ComparisonMeasureReferencePosition.java
+++ b/core/src/main/java/io/squashql/query/ComparisonMeasureReferencePosition.java
@@ -1,6 +1,7 @@
 package io.squashql.query;
 
 import io.squashql.query.dto.Period;
+import io.squashql.query.measure.visitor.MeasureVisitor;
 import lombok.*;
 
 import java.util.List;
@@ -115,4 +116,9 @@ public class ComparisonMeasureReferencePosition implements Measure {
   public String expression() {
     return this.expression;
   }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/DoubleConstantMeasure.java b/core/src/main/java/io/squashql/query/DoubleConstantMeasure.java
index d757024..93e0979 100644
--- a/core/src/main/java/io/squashql/query/DoubleConstantMeasure.java
+++ b/core/src/main/java/io/squashql/query/DoubleConstantMeasure.java
@@ -1,5 +1,6 @@
 package io.squashql.query;
 
+import io.squashql.query.measure.visitor.MeasureVisitor;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import lombok.NonNull;
@@ -27,4 +28,9 @@ public class DoubleConstantMeasure extends ConstantMeasure<Double> {
     measure.expression = expression;
     return measure;
   }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/ExpressionMeasure.java b/core/src/main/java/io/squashql/query/ExpressionMeasure.java
index 752030c..5e59ac5 100644
--- a/core/src/main/java/io/squashql/query/ExpressionMeasure.java
+++ b/core/src/main/java/io/squashql/query/ExpressionMeasure.java
@@ -1,5 +1,6 @@
 package io.squashql.query;
 
+import io.squashql.query.measure.visitor.MeasureVisitor;
 import lombok.*;
 
 @ToString
@@ -22,4 +23,8 @@ public class ExpressionMeasure implements BasicMeasure {
     return this.expression;
   }
 
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/Functions.java b/core/src/main/java/io/squashql/query/Functions.java
index 6fb2723..8900772 100644
--- a/core/src/main/java/io/squashql/query/Functions.java
+++ b/core/src/main/java/io/squashql/query/Functions.java
@@ -245,4 +245,18 @@ public class Functions {
   public static Field month(String field) {
     return new FunctionField("MONTH", new TableField(field));
   }
+
+  public static Measure comparisonMeasureWithParentOfAxis(String alias,
+                                                          ComparisonMethod comparisonMethod,
+                                                          Measure underlyingMeasure,
+                                                          Axis axis) {
+    return new PartialHierarchicalComparisonMeasure(alias, comparisonMethod, underlyingMeasure, axis, false);
+  }
+
+  public static Measure comparisonMeasureWithTotalOfAxis(String alias,
+                                                         ComparisonMethod comparisonMethod,
+                                                         Measure underlyingMeasure,
+                                                         Axis axis) {
+    return new PartialHierarchicalComparisonMeasure(alias, comparisonMethod, underlyingMeasure, axis, true);
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/LongConstantMeasure.java b/core/src/main/java/io/squashql/query/LongConstantMeasure.java
index 8160eac..ddfac50 100644
--- a/core/src/main/java/io/squashql/query/LongConstantMeasure.java
+++ b/core/src/main/java/io/squashql/query/LongConstantMeasure.java
@@ -1,5 +1,6 @@
 package io.squashql.query;
 
+import io.squashql.query.measure.visitor.MeasureVisitor;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import lombok.NonNull;
@@ -27,4 +28,9 @@ public class LongConstantMeasure extends ConstantMeasure<Long> {
     measure.expression = expression;
     return measure;
   }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/Measure.java b/core/src/main/java/io/squashql/query/Measure.java
index e2be64e..bb51064 100644
--- a/core/src/main/java/io/squashql/query/Measure.java
+++ b/core/src/main/java/io/squashql/query/Measure.java
@@ -1,6 +1,7 @@
 package io.squashql.query;
 
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import io.squashql.query.measure.visitor.MeasureVisitor;
 
 /**
  * Marker interface.
@@ -14,4 +15,5 @@ public interface Measure {
 
   Measure withExpression(String expression);
 
+  <R> R accept(MeasureVisitor<R> visitor);
 }
diff --git a/core/src/main/java/io/squashql/query/PartialHierarchicalComparisonMeasure.java b/core/src/main/java/io/squashql/query/PartialHierarchicalComparisonMeasure.java
new file mode 100644
index 0000000..b41db07
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/PartialHierarchicalComparisonMeasure.java
@@ -0,0 +1,48 @@
+package io.squashql.query;
+
+import io.squashql.query.measure.visitor.MeasureVisitor;
+import lombok.*;
+
+@ToString
+@EqualsAndHashCode
+@NoArgsConstructor // For Jackson
+@AllArgsConstructor
+public class PartialHierarchicalComparisonMeasure implements Measure {
+
+  public String alias;
+  @With
+  public String expression;
+  @Getter
+  public ComparisonMethod comparisonMethod;
+  @Getter
+  public Measure measure;
+  public Axis axis;
+  public boolean grandTotalAlongAncestors;
+
+  public PartialHierarchicalComparisonMeasure(String alias,
+                                              ComparisonMethod comparisonMethod,
+                                              Measure measure,
+                                              Axis axis,
+                                              boolean grandTotalAlongAncestors) {
+    this.alias = alias;
+    this.comparisonMethod = comparisonMethod;
+    this.measure = measure;
+    this.axis = axis;
+    this.grandTotalAlongAncestors = grandTotalAlongAncestors;
+  }
+
+  @Override
+  public String alias() {
+    return this.alias;
+  }
+
+  @Override
+  public String expression() {
+    return this.expression;
+  }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/QueryExecutor.java b/core/src/main/java/io/squashql/query/QueryExecutor.java
index 2620929..a59cda3 100644
--- a/core/src/main/java/io/squashql/query/QueryExecutor.java
+++ b/core/src/main/java/io/squashql/query/QueryExecutor.java
@@ -9,8 +9,9 @@ import io.squashql.query.dto.*;
 import io.squashql.query.join.ExperimentalQueryMergeExecutor;
 import io.squashql.query.parameter.QueryCacheParameter;
 import io.squashql.table.*;
-import io.squashql.table.PivotTableUtils.PivotTableContext;
+import io.squashql.table.PivotTableContext;
 import io.squashql.type.TypedField;
+import io.squashql.query.measure.visitor.PartialMeasureVisitor;
 import io.squashql.util.Queries;
 import lombok.extern.slf4j.Slf4j;
 
@@ -64,7 +65,7 @@ public class QueryExecutor {
 
     PivotTableContext pivotTableContext = new PivotTableContext(pivotTableQueryDto);
     QueryDto preparedQuery = PivotTableUtils.prepareQuery(pivotTableQueryDto.query, pivotTableContext);
-    Table result = executeQuery(preparedQuery, cacheStatsDtoBuilder, user, false, limitNotifier);
+    Table result = executeQuery(preparedQuery, cacheStatsDtoBuilder, user, false, limitNotifier, pivotTableContext);
     if (replaceTotalCellsAndOrderRows) {
       result = TableUtils.replaceTotalCellValues((ColumnarTable) result,
               pivotTableQueryDto.rows.stream().map(SqlUtils::squashqlExpression).toList(),
@@ -84,21 +85,29 @@ public class QueryExecutor {
     return this.queryEngine.executeRawSql(rawSqlQuery);
   }
 
+  /**
+   * Tabular API.
+   */
   public Table executeQuery(QueryDto query) {
     return executeQuery(
             query,
             CacheStatsDto.builder(),
             null,
             true,
-            null);
+            null,
+            createPivotTableContext(query));
   }
 
+  /**
+   * Tabular API. Tabular is a special case of Pivot Table
+   */
   public Table executeQuery(QueryDto query,
                             CacheStatsDto.CacheStatsDtoBuilder cacheStatsDtoBuilder,
                             SquashQLUser user,
                             boolean replaceTotalCellsAndOrderRows,
-                            IntConsumer limitNotifier) {
-    QueryDto preparedQuery = prepareQuery(query);
+                            IntConsumer limitNotifier,
+                            PivotTableContext pivotTableContext) {
+    QueryDto preparedQuery = prepareQuery(query, pivotTableContext);
 
     QueryResolver queryResolver = new QueryResolver(preparedQuery, this.queryEngine.datastore().storeByName());
     DependencyGraph<QueryPlanNodeKey> dependencyGraph = computeDependencyGraph(
@@ -202,7 +211,7 @@ public class QueryExecutor {
     return result;
   }
 
-  private static QueryDto prepareQuery(QueryDto query) {
+  private static QueryDto prepareQuery(QueryDto query, PivotTableContext pivotTableContext) {
     QueryDto deepCopy = query.clone();
     deepCopy.limit = query.limit < 0 ? LIMIT_DEFAULT_VALUE : query.limit;
 
@@ -217,6 +226,11 @@ public class QueryExecutor {
       });
     }
 
+    if (deepCopy.measures != null) {
+      PartialMeasureVisitor visitor = new PartialMeasureVisitor(pivotTableContext);
+      deepCopy.measures.replaceAll(m -> m.accept(visitor));
+    }
+
     return deepCopy;
   }
 
@@ -306,4 +320,8 @@ public class QueryExecutor {
     }
     return measures;
   }
+
+  public static PivotTableContext createPivotTableContext(QueryDto query) {
+    return new PivotTableContext(new PivotTableQueryDto(query, query.columns, Collections.emptyList(), Collections.emptyList()));
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/QueryMergeExecutor.java b/core/src/main/java/io/squashql/query/QueryMergeExecutor.java
index 3946e19..1f7ed5f 100644
--- a/core/src/main/java/io/squashql/query/QueryMergeExecutor.java
+++ b/core/src/main/java/io/squashql/query/QueryMergeExecutor.java
@@ -13,6 +13,8 @@ import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import static io.squashql.query.QueryExecutor.createPivotTableContext;
+
 public class QueryMergeExecutor {
 
   public static Table executeQueryMerge(QueryExecutor queryExecutor, QueryMergeDto queryMerge, SquashQLUser user) {
@@ -23,7 +25,9 @@ public class QueryMergeExecutor {
             false,
             limit -> {
               throw new LimitExceedException("Result of " + query + " is too big (limit=" + limit + ")");
-            });
+            },
+            createPivotTableContext(query)
+    );
     return execute(queryMerge,
             t -> (ColumnarTable) TableUtils.replaceTotalCellValues((ColumnarTable) t, true),
             executor);
diff --git a/core/src/main/java/io/squashql/query/QueryResolver.java b/core/src/main/java/io/squashql/query/QueryResolver.java
index 0c7e22d..e400977 100644
--- a/core/src/main/java/io/squashql/query/QueryResolver.java
+++ b/core/src/main/java/io/squashql/query/QueryResolver.java
@@ -85,7 +85,7 @@ public class QueryResolver {
   /**
    * Field resolver
    */
-  public TypedField resolveField(final Field field) {
+  public TypedField resolveField(Field field) {
     return this.cache.computeIfAbsent(field, f -> {
       // Special case for the column that is created due to the column set.
       ColumnSet columnSet = this.query.columnSets.get(GROUP);
@@ -418,17 +418,17 @@ public class QueryResolver {
     /**
      * We don't rely on Map.computeIfAbsent directly as it doesn't allow recursive updates
      */
-    private TypedField computeIfAbsent(final Field field, final Function<Field, TypedField> mappingFunction) {
+    private TypedField computeIfAbsent(Field field, Function<Field, TypedField> mappingFunction) {
       if (this.compiledFields.containsKey(field)) {
         return this.compiledFields.get(field);
       } else {
-        final TypedField typedField = mappingFunction.apply(field);
+        TypedField typedField = mappingFunction.apply(field);
         this.compiledFields.put(field, typedField);
         return typedField;
       }
     }
 
-    private CompiledMeasure computeIfAbsent(final Measure measure, final Function<Measure, CompiledMeasure> mappingFunction) {
+    private CompiledMeasure computeIfAbsent(Measure measure, Function<Measure, CompiledMeasure> mappingFunction) {
       if (this.compiledMeasure.containsKey(measure)) {
         return this.compiledMeasure.get(measure);
       } else {
@@ -438,7 +438,7 @@ public class QueryResolver {
       }
     }
 
-    private CompiledCriteria computeIfAbsent(final CriteriaDto criteria, final Function<CriteriaDto, CompiledCriteria> mappingFunction) {
+    private CompiledCriteria computeIfAbsent(CriteriaDto criteria, Function<CriteriaDto, CompiledCriteria> mappingFunction) {
       if (this.compiledCriteria.containsKey(criteria)) {
         return this.compiledCriteria.get(criteria);
       } else {
diff --git a/core/src/main/java/io/squashql/query/VectorAggMeasure.java b/core/src/main/java/io/squashql/query/VectorAggMeasure.java
index 0494bf2..3e54d99 100644
--- a/core/src/main/java/io/squashql/query/VectorAggMeasure.java
+++ b/core/src/main/java/io/squashql/query/VectorAggMeasure.java
@@ -1,5 +1,6 @@
 package io.squashql.query;
 
+import io.squashql.query.measure.visitor.MeasureVisitor;
 import lombok.*;
 
 @ToString
@@ -31,4 +32,9 @@ public class VectorAggMeasure implements Measure {
   public String expression() {
     return this.expression;
   }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/VectorTupleAggMeasure.java b/core/src/main/java/io/squashql/query/VectorTupleAggMeasure.java
index 8d2b823..6501840 100644
--- a/core/src/main/java/io/squashql/query/VectorTupleAggMeasure.java
+++ b/core/src/main/java/io/squashql/query/VectorTupleAggMeasure.java
@@ -1,5 +1,6 @@
 package io.squashql.query;
 
+import io.squashql.query.measure.visitor.MeasureVisitor;
 import lombok.*;
 
 import java.util.List;
@@ -43,4 +44,9 @@ public class VectorTupleAggMeasure implements Measure {
   public String expression() {
     return this.expression;
   }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledAggregatedMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledAggregatedMeasure.java
index 99edd2c..bb96e7a 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledAggregatedMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledAggregatedMeasure.java
@@ -38,7 +38,7 @@ public record CompiledAggregatedMeasure(String alias,
   }
 
   @Override
-  public <R> R accept(MeasureVisitor<R> visitor) {
+  public <R> R accept(CompiledMeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledBinaryOperationMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledBinaryOperationMeasure.java
index 1bb7576..1954000 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledBinaryOperationMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledBinaryOperationMeasure.java
@@ -22,7 +22,7 @@ public record CompiledBinaryOperationMeasure(String alias,
   }
 
   @Override
-  public <R> R accept(MeasureVisitor<R> visitor) {
+  public <R> R accept(CompiledMeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java b/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java
index 677622f..b7b52c9 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java
@@ -32,7 +32,7 @@ public record CompiledComparisonMeasureReferencePosition(String alias,
   }
 
   @Override
-  public <R> R accept(MeasureVisitor<R> visitor) {
+  public <R> R accept(CompiledMeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledDoubleConstantMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledDoubleConstantMeasure.java
index ab0ccd8..9959b95 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledDoubleConstantMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledDoubleConstantMeasure.java
@@ -15,7 +15,7 @@ public record CompiledDoubleConstantMeasure(Double value) implements CompiledMea
   }
 
   @Override
-  public <R> R accept(MeasureVisitor<R> visitor) {
+  public <R> R accept(CompiledMeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledExpressionMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledExpressionMeasure.java
index 5d43c53..7189d49 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledExpressionMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledExpressionMeasure.java
@@ -23,7 +23,7 @@ public record CompiledExpressionMeasure(String alias, String expression) impleme
   }
 
   @Override
-  public <R> R accept(MeasureVisitor<R> visitor) {
+  public <R> R accept(CompiledMeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledGrandTotalComparisonMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledGrandTotalComparisonMeasure.java
index 1e951e2..04c5150 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledGrandTotalComparisonMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledGrandTotalComparisonMeasure.java
@@ -25,7 +25,7 @@ public record CompiledGrandTotalComparisonMeasure(String alias,
   }
 
   @Override
-  public <R> R accept(MeasureVisitor<R> visitor) {
+  public <R> R accept(CompiledMeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledLongConstantMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledLongConstantMeasure.java
index 25000eb..9493962 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledLongConstantMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledLongConstantMeasure.java
@@ -15,7 +15,7 @@ public record CompiledLongConstantMeasure(Long value) implements CompiledMeasure
   }
 
   @Override
-  public <R> R accept(MeasureVisitor<R> visitor) {
+  public <R> R accept(CompiledMeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledMeasure.java
index 14be931..d6b1986 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledMeasure.java
@@ -8,5 +8,5 @@ public interface CompiledMeasure {
 
   String alias();
 
-  <R> R accept(MeasureVisitor<R> visitor);
+  <R> R accept(CompiledMeasureVisitor<R> visitor);
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java b/core/src/main/java/io/squashql/query/compiled/CompiledMeasureVisitor.java
similarity index 67%
rename from core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java
rename to core/src/main/java/io/squashql/query/compiled/CompiledMeasureVisitor.java
index 882f78f..eaf13f3 100644
--- a/core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledMeasureVisitor.java
@@ -1,6 +1,6 @@
 package io.squashql.query.compiled;
 
-public interface MeasureVisitor<R> {
+public interface CompiledMeasureVisitor<R> {
 
   R visit(CompiledAggregatedMeasure measure);
 
@@ -10,7 +10,7 @@ public interface MeasureVisitor<R> {
 
   R visit(CompiledComparisonMeasureReferencePosition measure);
 
-  R visit(CompiledGrandTotalComparisonMeasure compiledGrandTotalComparisonMeasure);
+  R visit(CompiledGrandTotalComparisonMeasure measure);
 
   R visit(CompiledDoubleConstantMeasure measure);
 
@@ -18,5 +18,5 @@ public interface MeasureVisitor<R> {
 
   R visit(CompiledVectorAggMeasure measure);
 
-  R visit(CompiledVectorTupleAggMeasure compiledVectorTupleAggMeasure);
+  R visit(CompiledVectorTupleAggMeasure measure);
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledVectorAggMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledVectorAggMeasure.java
index 4d888e8..1264ed7 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledVectorAggMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledVectorAggMeasure.java
@@ -19,7 +19,7 @@ public record CompiledVectorAggMeasure(String alias,
   }
 
   @Override
-  public <R> R accept(MeasureVisitor<R> visitor) {
+  public <R> R accept(CompiledMeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledVectorTupleAggMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledVectorTupleAggMeasure.java
index 4c7c975..733665e 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledVectorTupleAggMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledVectorTupleAggMeasure.java
@@ -22,7 +22,7 @@ public record CompiledVectorTupleAggMeasure(String alias,
   }
 
   @Override
-  public <R> R accept(MeasureVisitor<R> visitor) {
+  public <R> R accept(CompiledMeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/Evaluator.java b/core/src/main/java/io/squashql/query/compiled/Evaluator.java
index d20d3f3..872b453 100644
--- a/core/src/main/java/io/squashql/query/compiled/Evaluator.java
+++ b/core/src/main/java/io/squashql/query/compiled/Evaluator.java
@@ -18,7 +18,7 @@ import java.util.function.BiFunction;
 
 import static io.squashql.query.ColumnSetKey.GROUP;
 
-public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>, MeasureVisitor<Void> {
+public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>, CompiledMeasureVisitor<Void> {
 
   private ExecutionContext executionContext;
 
diff --git a/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java b/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java
index 4790ab4..d0d9a91 100644
--- a/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java
+++ b/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java
@@ -15,7 +15,7 @@ import java.util.stream.Stream;
 import static io.squashql.query.agg.AggregationFunction.*;
 
 @RequiredArgsConstructor
-public class PrefetchVisitor implements MeasureVisitor<Map<QueryScope, Set<CompiledMeasure>>> {
+public class PrefetchVisitor implements CompiledMeasureVisitor<Map<QueryScope, Set<CompiledMeasure>>> {
 
   private final List<TypedField> columns;
   private final List<TypedField> groupColumns;
@@ -124,14 +124,14 @@ public class PrefetchVisitor implements MeasureVisitor<Map<QueryScope, Set<Compi
   }
 
   @Override
-  public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledVectorTupleAggMeasure vecMeasure) {
-    TypedField vectorAxis = vecMeasure.vectorAxis();
-    List<TypedField> fieldToAggregates = vecMeasure.fieldToAggregateAndAggFunc().stream().map(CompiledFieldAndAggFunc::field).toList();
-    List<String> vectorAggFuncs = vecMeasure.fieldToAggregateAndAggFunc().stream().map(CompiledFieldAndAggFunc::aggFunc).toList();
+  public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledVectorTupleAggMeasure measure) {
+    TypedField vectorAxis = measure.vectorAxis();
+    List<TypedField> fieldToAggregates = measure.fieldToAggregateAndAggFunc().stream().map(CompiledFieldAndAggFunc::field).toList();
+    List<String> vectorAggFuncs = measure.fieldToAggregateAndAggFunc().stream().map(CompiledFieldAndAggFunc::aggFunc).toList();
     if (this.originalQueryScope.columns().contains(vectorAxis)) {
       Set<CompiledMeasure> m = new HashSet<>();
       for (int i = 0; i < fieldToAggregates.size(); i++) {
-        m.add(new CompiledAggregatedMeasure(vecMeasure.alias(), fieldToAggregates.get(i), vectorAggFuncs.get(i), null, false));
+        m.add(new CompiledAggregatedMeasure(measure.alias(), fieldToAggregates.get(i), vectorAggFuncs.get(i), null, false));
       }
       return Map.of(this.originalQueryScope, m);
     } else {
@@ -212,7 +212,7 @@ public class PrefetchVisitor implements MeasureVisitor<Map<QueryScope, Set<Compi
 
       int size = subQueryMeasureAliases.size();
       for (int i = 0; i < size; i++) {
-        String alias = size > 1 ? vecMeasure.alias() + "_" + i : vecMeasure.alias();
+        String alias = size > 1 ? measure.alias() + "_" + i : measure.alias();
         topQueryMeasures.add(new CompiledAggregatedMeasure(alias, new AliasedTypedField(subQueryMeasureAliases.get(i)), ARRAY_AGG, null, false));
       }
       return Map.of(topQueryScope, topQueryMeasures);
diff --git a/core/src/main/java/io/squashql/query/compiled/PrimitiveMeasureVisitor.java b/core/src/main/java/io/squashql/query/compiled/PrimitiveMeasureVisitor.java
index eb1970e..0326af5 100644
--- a/core/src/main/java/io/squashql/query/compiled/PrimitiveMeasureVisitor.java
+++ b/core/src/main/java/io/squashql/query/compiled/PrimitiveMeasureVisitor.java
@@ -1,6 +1,6 @@
 package io.squashql.query.compiled;
 
-public class PrimitiveMeasureVisitor implements MeasureVisitor<Boolean> {
+public class PrimitiveMeasureVisitor implements CompiledMeasureVisitor<Boolean> {
 
   @Override
   public Boolean visit(CompiledAggregatedMeasure measure) {
@@ -44,7 +44,7 @@ public class PrimitiveMeasureVisitor implements MeasureVisitor<Boolean> {
   }
 
   @Override
-  public Boolean visit(CompiledVectorTupleAggMeasure compiledVectorTupleAggMeasure) {
+  public Boolean visit(CompiledVectorTupleAggMeasure measure) {
     // This measure is "indirectly" computed by the underlying DB, but it is not a primitive. See PrefetchVisitor.
     return false;
   }
diff --git a/core/src/main/java/io/squashql/query/measure/ParametrizedMeasure.java b/core/src/main/java/io/squashql/query/measure/ParametrizedMeasure.java
index 8f4e44c..278ea6b 100644
--- a/core/src/main/java/io/squashql/query/measure/ParametrizedMeasure.java
+++ b/core/src/main/java/io/squashql/query/measure/ParametrizedMeasure.java
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import io.squashql.jackson.ParametrizedMeasureDeserializer;
 import io.squashql.jackson.ParametrizedMeasureSerializer;
 import io.squashql.query.Measure;
+import io.squashql.query.measure.visitor.MeasureVisitor;
 import lombok.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
@@ -40,4 +41,9 @@ public class ParametrizedMeasure implements Measure {
   public Measure withExpression(String __) {
     return this;
   }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(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
index a6bf2d7..2f77e95 100644
--- a/core/src/main/java/io/squashql/query/measure/ParametrizedMeasureFactory.java
+++ b/core/src/main/java/io/squashql/query/measure/ParametrizedMeasureFactory.java
@@ -24,7 +24,7 @@ public final class ParametrizedMeasureFactory {
   }
 
   /**
-   * Called by reflection.
+   * Called by reflection. See {@link Repository#create(ParametrizedMeasure)}
    */
   public static Measure var(String alias, Field value, Field date, double quantile) {
     // The function that computes the VaR for a given quantile
@@ -47,7 +47,7 @@ public final class ParametrizedMeasureFactory {
   }
 
   /**
-   * Called by reflection.
+   * Called by reflection. See {@link Repository#create(ParametrizedMeasure)}
    */
   public static Measure incrementalVar(String alias, Field value, Field date, double quantile, List<Field> ancestors) {
     Measure vector = new VectorTupleAggMeasure("__vector__",
diff --git a/core/src/main/java/io/squashql/query/measure/Repository.java b/core/src/main/java/io/squashql/query/measure/Repository.java
index 8cf09da..f31198e 100644
--- a/core/src/main/java/io/squashql/query/measure/Repository.java
+++ b/core/src/main/java/io/squashql/query/measure/Repository.java
@@ -2,9 +2,10 @@ package io.squashql.query.measure;
 
 import com.fasterxml.jackson.databind.JavaType;
 import io.squashql.jackson.SquashQLTypeFactory;
+import io.squashql.query.Axis;
 import io.squashql.query.Field;
 import io.squashql.query.Measure;
-import org.eclipse.collections.api.tuple.Pair;
+import org.eclipse.collections.api.tuple.Triple;
 import org.eclipse.collections.impl.tuple.Tuples;
 
 import java.lang.reflect.InvocationTargetException;
@@ -21,36 +22,48 @@ public final class Repository {
   private Repository() {
   }
 
-  public static Measure create(ParametrizedMeasure m) {
-    String methodName = switch (m.key) {
+  public static Measure create(ParametrizedMeasure pm) {
+    String methodName = switch (pm.key) {
       case VAR -> "var";
       case INCREMENTAL_VAR -> "incrementalVar";
-      default -> throw new IllegalArgumentException("unknown " + ParametrizedMeasure.class + ": " + m);
+      default -> throw new IllegalArgumentException("unknown " + ParametrizedMeasure.class + ": " + pm);
     };
 
     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();
+      Method method = Arrays.stream(ParametrizedMeasureFactory.class.getDeclaredMethods()).filter(m -> m.getName().equals(methodName)).findFirst().get();
+      List<String> parameterNames = getParameterTypes(pm.key).stream().map(Triple::getOne).toList();
+      List<Boolean> isMandatory = getParameterTypes(pm.key).stream().map(Triple::getThree).toList();
       List<Object> args = new ArrayList<>();
-      args.add(m.alias);
-      for (String paramName : parameterNames) {
-        args.add(m.parameters.get(paramName));
+      args.add(pm.alias);
+      for (int i = 0; i < parameterNames.size(); i++) {
+        Object e = pm.parameters.get(parameterNames.get(i));
+        if (e == null) {
+          if (isMandatory.get(i)) {
+            throw new IllegalArgumentException(String.format("Parameter '%s' was expected but not provided in %s", parameterNames.get(i), pm));
+          }
+          continue;
+        }
+        args.add(e);
       }
-      return (Measure) var.invoke(null, args.toArray(new Object[0]));
+      return (Measure) method.invoke(null, args.toArray(new Object[0]));
     } catch (IllegalAccessException |
              InvocationTargetException e) {
       throw new RuntimeException(e);
     }
   }
 
-  public static List<Pair<String, JavaType>> getParameterTypes(String key) {
+  /**
+   * Boolean -> is mandatory?
+   */
+  public static List<Triple<String, JavaType, Boolean>> getParameterTypes(String key) {
     JavaType fieldJT = SquashQLTypeFactory.of(Field.class);
     JavaType doubleJT = SquashQLTypeFactory.of(double.class);
     JavaType listOfFieldsJT = SquashQLTypeFactory.listOf(Field.class);
+    JavaType axis = SquashQLTypeFactory.of(Axis.class);
     if (key.equals(VAR)) {
-      return List.of(Tuples.pair("value", fieldJT), Tuples.pair("date", fieldJT), Tuples.pair("quantile", doubleJT));
+      return List.of(Tuples.triple("value", fieldJT, true), Tuples.triple("date", fieldJT, true), Tuples.triple("quantile", doubleJT, true));
     } 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));
+      return List.of(Tuples.triple("value", fieldJT, true), Tuples.triple("date", fieldJT, true), Tuples.triple("quantile", doubleJT, true), Tuples.triple("ancestors", listOfFieldsJT, true), Tuples.triple("axis", axis, false));
     } else {
       throw new IllegalArgumentException("unknown key: " + key);
     }
diff --git a/core/src/main/java/io/squashql/query/measure/visitor/MeasureVisitor.java b/core/src/main/java/io/squashql/query/measure/visitor/MeasureVisitor.java
new file mode 100644
index 0000000..1e3802a
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/measure/visitor/MeasureVisitor.java
@@ -0,0 +1,29 @@
+package io.squashql.query.measure.visitor;
+
+import io.squashql.query.*;
+import io.squashql.query.measure.ParametrizedMeasure;
+
+public interface MeasureVisitor<R> {
+
+  R visit(AggregatedMeasure measure);
+
+  R visit(ExpressionMeasure measure);
+
+  R visit(BinaryOperationMeasure measure);
+
+  R visit(ComparisonMeasureReferencePosition measure);
+
+  R visit(ComparisonMeasureGrandTotal measure);
+
+  R visit(DoubleConstantMeasure measure);
+
+  R visit(LongConstantMeasure measure);
+
+  R visit(VectorAggMeasure measure);
+
+  R visit(VectorTupleAggMeasure measure);
+
+  R visit(ParametrizedMeasure measure);
+
+  R visit(PartialHierarchicalComparisonMeasure measure);
+}
diff --git a/core/src/main/java/io/squashql/query/measure/visitor/PartialMeasureVisitor.java b/core/src/main/java/io/squashql/query/measure/visitor/PartialMeasureVisitor.java
new file mode 100644
index 0000000..a0f947f
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/measure/visitor/PartialMeasureVisitor.java
@@ -0,0 +1,112 @@
+package io.squashql.query.measure.visitor;
+
+import io.squashql.query.*;
+import io.squashql.query.measure.ParametrizedMeasure;
+import io.squashql.query.measure.Repository;
+import io.squashql.table.PivotTableContext;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public record PartialMeasureVisitor(
+        PivotTableContext pivotTableContext) implements MeasureVisitor<Measure> {
+
+  @Override
+  public Measure visit(AggregatedMeasure measure) {
+    return measure;
+  }
+
+  @Override
+  public Measure visit(ExpressionMeasure measure) {
+    return measure;
+  }
+
+  @Override
+  public Measure visit(BinaryOperationMeasure measure) {
+    return new BinaryOperationMeasure(measure.alias,
+            measure.operator,
+            measure.leftOperand.accept(this),
+            measure.rightOperand.accept(this));
+  }
+
+  @Override
+  public Measure visit(ComparisonMeasureReferencePosition measure) {
+    return new ComparisonMeasureReferencePosition(
+            measure.alias,
+            measure.expression,
+            measure.comparisonMethod,
+            measure.comparisonOperator,
+            measure.measure.accept(this),
+            measure.columnSetKey,
+            measure.elements,
+            measure.referencePosition,
+            measure.period,
+            measure.ancestors,
+            measure.grandTotalAlongAncestors);
+  }
+
+  @Override
+  public Measure visit(ComparisonMeasureGrandTotal measure) {
+    return new ComparisonMeasureGrandTotal(
+            measure.alias,
+            measure.expression,
+            measure.comparisonMethod,
+            measure.measure.accept(this));
+  }
+
+  @Override
+  public Measure visit(DoubleConstantMeasure measure) {
+    return measure;
+  }
+
+  @Override
+  public Measure visit(LongConstantMeasure measure) {
+    return measure;
+  }
+
+  @Override
+  public Measure visit(VectorAggMeasure measure) {
+    return measure;
+  }
+
+  @Override
+  public Measure visit(VectorTupleAggMeasure measure) {
+    return measure;
+  }
+
+  @Override
+  public Measure visit(ParametrizedMeasure measure) {
+    if (measure.key.equals(Repository.INCREMENTAL_VAR)) {
+      Object axis = measure.parameters.get("axis");
+      if (axis != null || !measure.parameters.containsKey("ancestors")) {
+        List<Field> ancestors = getAncestors((Axis) axis);
+        Map<String, Object> copy = new HashMap<>(measure.parameters);
+        copy.remove("axis");
+        copy.put("ancestors", ancestors);
+        return new ParametrizedMeasure(measure.alias, Repository.INCREMENTAL_VAR, copy);
+      }
+    }
+    return measure;
+  }
+
+  @Override
+  public Measure visit(PartialHierarchicalComparisonMeasure measure) {
+    List<Field> ancestors = getAncestors(measure.axis);
+    return new ComparisonMeasureReferencePosition(
+            measure.alias,
+            measure.comparisonMethod,
+            measure.measure.accept(this),
+            ancestors,
+            measure.grandTotalAlongAncestors
+    );
+  }
+
+  private List<Field> getAncestors(Axis axis) {
+    List<Field> ancestors = axis != null ? switch (axis) {
+      case ROW -> this.pivotTableContext.cleansedColumns;
+      case COLUMN -> this.pivotTableContext.cleansedRows;
+    } : this.pivotTableContext.cleansedColumns;
+    return ancestors;
+  }
+}
diff --git a/core/src/main/java/io/squashql/table/PivotTableContext.java b/core/src/main/java/io/squashql/table/PivotTableContext.java
new file mode 100644
index 0000000..9612f2a
--- /dev/null
+++ b/core/src/main/java/io/squashql/table/PivotTableContext.java
@@ -0,0 +1,43 @@
+package io.squashql.table;
+
+import io.squashql.query.ColumnSet;
+import io.squashql.query.Field;
+import io.squashql.query.dto.GroupColumnSetDto;
+import io.squashql.query.dto.PivotTableQueryDto;
+import io.squashql.query.dto.QueryDto;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static io.squashql.query.ColumnSetKey.GROUP;
+
+public class PivotTableContext {
+  public final List<Field> rows;
+  public final List<Field> cleansedRows;
+  public final List<Field> columns;
+  public final List<Field> cleansedColumns;
+  public final List<Field> hiddenTotals;
+
+  public PivotTableContext(PivotTableQueryDto pivotTableQueryDto) {
+    this.hiddenTotals = pivotTableQueryDto.hiddenTotals == null ? Collections.emptyList() : pivotTableQueryDto.hiddenTotals;
+    this.rows = pivotTableQueryDto.rows;
+    this.cleansedRows = cleanse(pivotTableQueryDto.query, pivotTableQueryDto.rows);
+    this.columns = pivotTableQueryDto.columns;
+    this.cleansedColumns = cleanse(pivotTableQueryDto.query, pivotTableQueryDto.columns);
+  }
+
+  public static List<Field> cleanse(QueryDto query, List<Field> fields) {
+    // ColumnSet is a special type of column that does not exist in the database but only in SquashQL. Totals can't be
+    // computed. This is why it is removed from the axes.
+    ColumnSet columnSet = query.columnSets.get(GROUP);
+    if (columnSet != null) {
+      Field newField = ((GroupColumnSetDto) columnSet).newField;
+      if (fields.contains(newField)) {
+        fields = new ArrayList<>(fields);
+        fields.remove(newField);
+      }
+    }
+    return fields;
+  }
+}
diff --git a/core/src/main/java/io/squashql/table/PivotTableUtils.java b/core/src/main/java/io/squashql/table/PivotTableUtils.java
index 192f9c1..33a5e2d 100644
--- a/core/src/main/java/io/squashql/table/PivotTableUtils.java
+++ b/core/src/main/java/io/squashql/table/PivotTableUtils.java
@@ -1,11 +1,8 @@
 package io.squashql.table;
 
-import io.squashql.query.ColumnSet;
 import io.squashql.query.Field;
 import io.squashql.query.Header;
 import io.squashql.query.database.SqlUtils;
-import io.squashql.query.dto.GroupColumnSetDto;
-import io.squashql.query.dto.PivotTableQueryDto;
 import io.squashql.query.dto.QueryDto;
 import io.squashql.util.ListUtils;
 import io.squashql.util.NullAndTotalComparator;
@@ -15,8 +12,6 @@ import java.util.*;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
-import static io.squashql.query.ColumnSetKey.GROUP;
-
 public final class PivotTableUtils {
 
   private PivotTableUtils() {
@@ -344,34 +339,4 @@ public final class PivotTableUtils {
       return Arrays.toString(this.a);
     }
   }
-
-  public static class PivotTableContext {
-    private final List<Field> rows;
-    private final List<Field> cleansedRows;
-    private final List<Field> columns;
-    private final List<Field> cleansedColumns;
-    private final List<Field> hiddenTotals;
-
-    public PivotTableContext(PivotTableQueryDto pivotTableQueryDto) {
-      this.hiddenTotals = pivotTableQueryDto.hiddenTotals == null ? Collections.emptyList() : pivotTableQueryDto.hiddenTotals;
-      this.rows = pivotTableQueryDto.rows;
-      this.cleansedRows = cleanse(pivotTableQueryDto.query, pivotTableQueryDto.rows);
-      this.columns = pivotTableQueryDto.columns;
-      this.cleansedColumns = cleanse(pivotTableQueryDto.query, pivotTableQueryDto.columns);
-    }
-
-    public static List<Field> cleanse(QueryDto query, List<Field> fields) {
-      // ColumnSet is a special type of column that does not exist in the database but only in SquashQL. Totals can't be
-      // computed. This is why it is removed from the axes.
-      ColumnSet columnSet = query.columnSets.get(GROUP);
-      if (columnSet != null) {
-        Field newField = ((GroupColumnSetDto) columnSet).newField;
-        if (fields.contains(newField)) {
-          fields = new ArrayList<>(fields);
-          fields.remove(newField);
-        }
-      }
-      return fields;
-    }
-  }
 }
diff --git a/js/typescript-library/src/generate-json-queryDto.ts b/js/typescript-library/src/generate-json-queryDto.ts
index f9f7882..a06ce59 100644
--- a/js/typescript-library/src/generate-json-queryDto.ts
+++ b/js/typescript-library/src/generate-json-queryDto.ts
@@ -2,24 +2,30 @@ import {JoinType, Query, Table} from "./query"
 import {
   AggregatedMeasure,
   avgIf,
+  Axis,
   BinaryOperationMeasure,
   BinaryOperator,
   comparisonMeasureWithGrandTotal,
   comparisonMeasureWithGrandTotalAlongAncestors,
-  comparisonMeasureWithinSameGroup, comparisonMeasureWithinSameGroupInOrder,
+  comparisonMeasureWithinSameGroup,
+  comparisonMeasureWithinSameGroupInOrder,
   comparisonMeasureWithParent,
-  comparisonMeasureWithPeriod,
+  comparisonMeasureWithParentOfAxis,
+  comparisonMeasureWithPeriod, comparisonMeasureWithTotalOfAxis,
   ComparisonMethod,
   decimal,
   ExpressionMeasure,
-  integer, ParametrizedMeasure,
+  integer,
+  ParametrizedMeasure,
   sum,
   totalCount,
 } from "./measure"
 import {
   _in,
   all,
-  and, ConditionType, contains,
+  and,
+  ConditionType,
+  contains,
   criterion,
   criterion_,
   eq,
@@ -95,6 +101,18 @@ export function generateFromQueryDto() {
     "quantile": 0.95,
     "ancestors": [tableField("f1"), tableField("f2"), tableField("f3")],
   }))
+  q.withMeasure(comparisonMeasureWithParentOfAxis(
+          "comp parent of column",
+          ComparisonMethod.DIVIDE,
+          countRows,
+          Axis.COLUMN
+  ))
+  q.withMeasure(comparisonMeasureWithTotalOfAxis(
+          "comp total of row",
+          ComparisonMethod.DIVIDE,
+          countRows,
+          Axis.ROW
+  ))
 
   const queryCondition = or(or(and(eq("a"), eq("b")), lt(5)), like("a%"))
   q.withWhereCriteria(all([
diff --git a/js/typescript-library/src/index.ts b/js/typescript-library/src/index.ts
index 6369ec3..36633b3 100644
--- a/js/typescript-library/src/index.ts
+++ b/js/typescript-library/src/index.ts
@@ -16,6 +16,8 @@ export {
   AggregatedMeasure,
   ExpressionMeasure,
   ParametrizedMeasure,
+  PartialHierarchicalComparisonMeasure,
+  Axis,
   sum,
   min,
   max,
@@ -40,6 +42,8 @@ export {
   comparisonMeasureWithParent,
   comparisonMeasureWithGrandTotalAlongAncestors,
   comparisonMeasureWithGrandTotal,
+  comparisonMeasureWithParentOfAxis,
+  comparisonMeasureWithTotalOfAxis,
   totalCount,
   ComparisonMethod,
   BinaryOperator,
diff --git a/js/typescript-library/src/measure.ts b/js/typescript-library/src/measure.ts
index 053d508..4d9ba40 100644
--- a/js/typescript-library/src/measure.ts
+++ b/js/typescript-library/src/measure.ts
@@ -135,6 +135,34 @@ export class ComparisonMeasureGrandTotal implements Measure {
   }
 }
 
+export enum Axis {
+  ROW = "ROW",
+  COLUMN = "COLUMN",
+}
+
+export class PartialHierarchicalComparisonMeasure implements Measure {
+  readonly class: string = PACKAGE + "PartialHierarchicalComparisonMeasure"
+  readonly expression?: string
+
+  constructor(readonly alias: string,
+              readonly comparisonMethod: ComparisonMethod,
+              readonly measure: Measure,
+              readonly axis: Axis,
+              readonly grandTotalAlongAncestors: boolean) {
+  }
+
+  toJSON() {
+    return {
+      "@class": this.class,
+      "alias": this.alias,
+      "comparisonMethod": this.comparisonMethod,
+      "measure": this.measure,
+      "axis": this.axis,
+      "grandTotalAlongAncestors": this.grandTotalAlongAncestors,
+    }
+  }
+}
+
 export class ParametrizedMeasure implements Measure {
   readonly class: string = PACKAGE + "measure.ParametrizedMeasure"
   readonly expression?: string
@@ -321,3 +349,17 @@ export function comparisonMeasureWithGrandTotal(alias: string,
                                                 measure: Measure): Measure {
   return new ComparisonMeasureGrandTotal(alias, comparisonMethod, measure)
 }
+
+export function comparisonMeasureWithParentOfAxis(alias: string,
+                                                  comparisonMethod: ComparisonMethod,
+                                                  measure: Measure,
+                                                  axis: Axis) {
+  return new PartialHierarchicalComparisonMeasure(alias, comparisonMethod, measure, axis, false)
+}
+
+export function comparisonMeasureWithTotalOfAxis(alias: string,
+                                                 comparisonMethod: ComparisonMethod,
+                                                 measure: Measure,
+                                                 axis: Axis) {
+  return new PartialHierarchicalComparisonMeasure(alias, comparisonMethod, measure, axis, true)
+}
diff --git a/server/src/main/java/io/squashql/spring/web/rest/QueryController.java b/server/src/main/java/io/squashql/spring/web/rest/QueryController.java
index e713f71..ca742db 100644
--- a/server/src/main/java/io/squashql/spring/web/rest/QueryController.java
+++ b/server/src/main/java/io/squashql/spring/web/rest/QueryController.java
@@ -19,6 +19,8 @@ import java.util.*;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
+import static io.squashql.query.QueryExecutor.createPivotTableContext;
+
 @Import({JacksonConfiguration.class, SquashQLErrorHandler.class})
 @RestController
 public class QueryController {
@@ -51,7 +53,8 @@ public class QueryController {
             csBuilder,
             this.squashQLUserSupplier == null ? null : this.squashQLUserSupplier.get(),
             true,
-            null);
+            null,
+            createPivotTableContext(query));
     return ResponseEntity.ok(createQueryResultDto(table, csBuilder, query.minify));
   }
 


Test patch:
diff --git a/bigquery/src/test/java/io/squashql/query/TestBigQueryPartialHierarchicalMeasureComparison.java b/bigquery/src/test/java/io/squashql/query/TestBigQueryPartialHierarchicalMeasureComparison.java
new file mode 100644
index 0000000..f1f61e3
--- /dev/null
+++ b/bigquery/src/test/java/io/squashql/query/TestBigQueryPartialHierarchicalMeasureComparison.java
@@ -0,0 +1,51 @@
+package io.squashql.query;
+
+import io.squashql.BigQueryDatastore;
+import io.squashql.BigQueryServiceAccountDatastore;
+import io.squashql.query.database.BigQueryEngine;
+import io.squashql.query.database.QueryEngine;
+import io.squashql.store.Datastore;
+import io.squashql.transaction.BigQueryDataLoader;
+import io.squashql.transaction.DataLoader;
+import org.junit.jupiter.api.AfterAll;
+
+import static io.squashql.query.BigQueryTestUtil.DATASET_NAME;
+import static io.squashql.query.BigQueryTestUtil.PROJECT_ID;
+
+/**
+ * Do not edit this class, it has been generated automatically by {@link io.squashql.template.BigQueryClassTemplateGenerator}.
+ */
+public class TestBigQueryPartialHierarchicalMeasureComparison extends ATestPartialHierarchicalMeasureComparison {
+
+  @AfterAll
+  void tearDown() {
+    this.fieldsByStore.forEach((store, fields) -> BigQueryTestUtil.deleteTable(((BigQueryDatastore) this.datastore).getBigquery(), store));
+  }
+
+  @Override
+  protected void createTables() {
+    BigQueryDataLoader tm = (BigQueryDataLoader) this.tm;
+    BigQueryTestUtil.createDatasetIfDoesNotExist(tm.getBigQuery(), DATASET_NAME);
+    this.fieldsByStore.forEach((store, fields) -> tm.dropAndCreateInMemoryTable(store, fields));
+  }
+
+  @Override
+  protected QueryEngine createQueryEngine(Datastore datastore) {
+    return new BigQueryEngine((BigQueryDatastore) datastore);
+  }
+
+  @Override
+  protected Datastore createDatastore() {
+    return new BigQueryServiceAccountDatastore(BigQueryTestUtil.createServiceAccountCredentials(), PROJECT_ID, DATASET_NAME);
+  }
+
+  @Override
+  protected DataLoader createDataLoader() {
+    return new BigQueryDataLoader(((BigQueryDatastore) this.datastore).getBigquery(), DATASET_NAME);
+  }
+
+  @Override
+  protected Object translate(Object o) {
+    return BigQueryTestUtil.translate(o);
+  }
+}
diff --git a/clickhouse/src/test/java/io/squashql/query/TestClickHousePartialHierarchicalMeasureComparison.java b/clickhouse/src/test/java/io/squashql/query/TestClickHousePartialHierarchicalMeasureComparison.java
new file mode 100644
index 0000000..4a75935
--- /dev/null
+++ b/clickhouse/src/test/java/io/squashql/query/TestClickHousePartialHierarchicalMeasureComparison.java
@@ -0,0 +1,53 @@
+package io.squashql.query;
+
+import io.squashql.ClickHouseDatastore;
+import io.squashql.query.database.ClickHouseQueryEngine;
+import io.squashql.query.database.QueryEngine;
+import io.squashql.store.Datastore;
+import io.squashql.template.ClickHouseClassTemplateGenerator;
+import io.squashql.transaction.ClickHouseDataLoader;
+import io.squashql.transaction.DataLoader;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+
+/**
+ * Do not edit this class, it has been generated automatically by {@link ClickHouseClassTemplateGenerator}.
+ */
+public class TestClickHousePartialHierarchicalMeasureComparison extends ATestPartialHierarchicalMeasureComparison {
+
+  public org.testcontainers.containers.GenericContainer container = ClickHouseTestUtil.createClickHouseContainer();
+
+  @BeforeAll
+  @Override
+  void setup() {
+    this.container.start();
+    super.setup();
+  }
+
+  @AfterAll
+  void tearDown() {
+    // we do not stop the container to be able to reuse it between tests.
+    ((ClickHouseDataLoader) this.tm).dropTables(this.fieldsByStore.keySet());
+  }
+
+  @Override
+  protected void createTables() {
+    ClickHouseDataLoader tm = (ClickHouseDataLoader) this.tm;
+    this.fieldsByStore.forEach((store, fields) -> tm.dropAndCreateInMemoryTable(store, fields));
+  }
+
+  @Override
+  protected QueryEngine createQueryEngine(Datastore datastore) {
+    return new ClickHouseQueryEngine((ClickHouseDatastore) datastore);
+  }
+
+  @Override
+  protected Datastore createDatastore() {
+    return new ClickHouseDatastore(ClickHouseTestUtil.jdbcUrl.apply(this.container));
+  }
+
+  @Override
+  protected DataLoader createDataLoader() {
+    return new ClickHouseDataLoader(((ClickHouseDatastore) this.datastore).dataSource);
+  }
+}
diff --git a/core/src/test/java/io/squashql/jackson/TestJacksonUtil.java b/core/src/test/java/io/squashql/jackson/TestJacksonUtil.java
index 377a6e1..d8461e1 100644
--- a/core/src/test/java/io/squashql/jackson/TestJacksonUtil.java
+++ b/core/src/test/java/io/squashql/jackson/TestJacksonUtil.java
@@ -1,16 +1,9 @@
 package io.squashql.jackson;
 
-import io.squashql.query.Field;
-import io.squashql.query.Measure;
-import io.squashql.query.TableField;
-import io.squashql.query.dto.QueryDto;
-import io.squashql.query.measure.ParametrizedMeasure;
-import io.squashql.query.measure.Repository;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import java.time.LocalDate;
-import java.util.Map;
 
 public class TestJacksonUtil {
 
@@ -21,27 +14,4 @@ public class TestJacksonUtil {
     LocalDate deserialize = JacksonUtil.deserialize(serialize, LocalDate.class);
     Assertions.assertThat(deserialize).isEqualTo(d);
   }
-
-  @Test
-  void testTableField() {
-    TableField tableField = new TableField("myTable.myField");
-    ParametrizedMeasure pm = new ParametrizedMeasure("var", Repository.VAR, Map.of("value", tableField));
-
-    QueryDto query = new QueryDto();
-    query.table("myTable");
-    query.withColumn(tableField);
-    query.withMeasure(pm);
-
-    String tableFieldSer = JacksonUtil.serialize(tableField);
-    Field tableFieldDes = JacksonUtil.deserialize(tableFieldSer, Field.class);
-    Assertions.assertThat(tableFieldDes).isEqualTo(tableField);
-
-    String pmSer = JacksonUtil.serialize(pm);
-    Measure pmDes = JacksonUtil.deserialize(pmSer, Measure.class);
-    Assertions.assertThat(pmDes).isEqualTo(pm);
-
-    String querySer = JacksonUtil.serialize(query);
-    QueryDto queryDes = JacksonUtil.deserialize(querySer, QueryDto.class);
-    Assertions.assertThat(queryDes).isEqualTo(query);
-  }
 }
diff --git a/core/src/test/java/io/squashql/jackson/TestQueryS13n.java b/core/src/test/java/io/squashql/jackson/TestQueryS13n.java
index bf06f84..318dcef 100644
--- a/core/src/test/java/io/squashql/jackson/TestQueryS13n.java
+++ b/core/src/test/java/io/squashql/jackson/TestQueryS13n.java
@@ -3,6 +3,8 @@ package io.squashql.jackson;
 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.Parameter;
 import io.squashql.query.parameter.QueryCacheParameter;
 import org.assertj.core.api.Assertions;
@@ -182,7 +184,7 @@ public class TestQueryS13n {
   void testTableField() {
     TableField fieldFullName = new TableField("table.name");
     TableField simpleNameField = new TableField("name");
-    TableField field = new TableField("table","name");
+    TableField field = new TableField("table", "name");
     TableField fieldFullNameDeserialize = JacksonUtil.deserialize(JacksonUtil.serialize(fieldFullName), TableField.class);
     Assertions.assertThat(fieldFullNameDeserialize).isEqualTo(fieldFullName);
     TableField fieldDeserialize = JacksonUtil.deserialize(JacksonUtil.serialize(field), TableField.class);
@@ -203,4 +205,47 @@ public class TestQueryS13n {
     PivotTableQueryDto deserialize = JacksonUtil.deserialize(serialize, PivotTableQueryDto.class);
     Assertions.assertThat(deserialize).isEqualTo(pivotTableQueryDto);
   }
+
+  @Test
+  void testParametrizedMeasure() {
+    TableField value = new TableField("myTable.value");
+    TableField date = new TableField("myTable.date");
+    ParametrizedMeasure pm = new ParametrizedMeasure("var measure", Repository.VAR, Map.of(
+            "value", value,
+            "date", date,
+            "quantile", 0.95));
+
+    QueryDto query = new QueryDto();
+    query.table("myTable");
+    query.withColumn(value);
+    query.withMeasure(pm);
+
+    String tableFieldSer = JacksonUtil.serialize(value);
+    Field tableFieldDes = JacksonUtil.deserialize(tableFieldSer, Field.class);
+    Assertions.assertThat(tableFieldDes).isEqualTo(value);
+
+    String pmSer = JacksonUtil.serialize(pm);
+    Measure pmDes = JacksonUtil.deserialize(pmSer, Measure.class);
+    Assertions.assertThat(pmDes).isEqualTo(pm);
+
+    String querySer = JacksonUtil.serialize(query);
+    QueryDto queryDes = JacksonUtil.deserialize(querySer, QueryDto.class);
+    Assertions.assertThat(queryDes).isEqualTo(query);
+  }
+
+  @Test
+  void testIncrVar() {
+    TableField value = new TableField("myTable.value");
+    TableField date = new TableField("myTable.date");
+    Measure incrementalVar = new ParametrizedMeasure("partial inc var",
+            Repository.INCREMENTAL_VAR, Map.of(
+            "value", value,
+            "date", date,
+            "quantile", 0.95,
+            "axis", Axis.COLUMN
+    ));
+    String pmSer = JacksonUtil.serialize(incrementalVar);
+    Measure pmDes = JacksonUtil.deserialize(pmSer, Measure.class);
+    Assertions.assertThat(pmDes).isEqualTo(incrementalVar);
+  }
 }
diff --git a/core/src/test/java/io/squashql/query/ATestParentComparison.java b/core/src/test/java/io/squashql/query/ATestParentComparison.java
index 318b316..808f43b 100644
--- a/core/src/test/java/io/squashql/query/ATestParentComparison.java
+++ b/core/src/test/java/io/squashql/query/ATestParentComparison.java
@@ -24,11 +24,11 @@ import static io.squashql.transaction.DataLoader.MAIN_SCENARIO_NAME;
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public abstract class ATestParentComparison extends ABaseTestQuery {
 
-  private String storeName = "store" + getClass().getSimpleName().toLowerCase();
-  private TableField city = new TableField(this.storeName, "city");
-  private TableField country = new TableField(this.storeName, "country");
-  private TableField continent = new TableField(this.storeName, "continent");
-  private TableField population = new TableField(this.storeName, "population");
+  private final String storeName = "store" + getClass().getSimpleName().toLowerCase();
+  private final TableField city = new TableField(this.storeName, "city");
+  private final TableField country = new TableField(this.storeName, "country");
+  private final TableField continent = new TableField(this.storeName, "continent");
+  private final TableField population = new TableField(this.storeName, "population");
 
   @Override
   protected Map<String, List<TableTypedField>> getFieldsByStore() {
diff --git a/core/src/test/java/io/squashql/query/ATestParentComparisonWithOtherColumn.java b/core/src/test/java/io/squashql/query/ATestParentComparisonWithOtherColumn.java
index 9c1dac6..ac50f89 100644
--- a/core/src/test/java/io/squashql/query/ATestParentComparisonWithOtherColumn.java
+++ b/core/src/test/java/io/squashql/query/ATestParentComparisonWithOtherColumn.java
@@ -20,7 +20,7 @@ import static io.squashql.transaction.DataLoader.MAIN_SCENARIO_NAME;
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public abstract class ATestParentComparisonWithOtherColumn extends ABaseTestQuery {
 
-  protected String storeName = "store" + getClass().getSimpleName().toLowerCase();
+  private final String storeName = "store" + getClass().getSimpleName().toLowerCase();
 
   @Override
   protected Map<String, List<TableTypedField>> getFieldsByStore() {
diff --git a/core/src/test/java/io/squashql/query/ATestPartialHierarchicalMeasureComparison.java b/core/src/test/java/io/squashql/query/ATestPartialHierarchicalMeasureComparison.java
new file mode 100644
index 0000000..2e75e6c
--- /dev/null
+++ b/core/src/test/java/io/squashql/query/ATestPartialHierarchicalMeasureComparison.java
@@ -0,0 +1,171 @@
+package io.squashql.query;
+
+import io.squashql.TestClass;
+import io.squashql.query.builder.Query;
+import io.squashql.query.dto.PivotTableQueryDto;
+import io.squashql.query.dto.QueryDto;
+import io.squashql.table.PivotTable;
+import io.squashql.table.Table;
+import io.squashql.type.TableTypedField;
+import io.squashql.util.TestUtil;
+import org.assertj.core.api.Assertions;
+import org.eclipse.collections.impl.list.mutable.FastList;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.TestInstance;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static io.squashql.query.Axis.COLUMN;
+import static io.squashql.query.Axis.ROW;
+import static io.squashql.query.ComparisonMethod.ABSOLUTE_DIFFERENCE;
+import static io.squashql.query.ComparisonMethod.DIVIDE;
+import static io.squashql.query.Functions.*;
+import static io.squashql.query.database.QueryEngine.GRAND_TOTAL;
+import static io.squashql.query.database.QueryEngine.TOTAL;
+import static io.squashql.transaction.DataLoader.MAIN_SCENARIO_NAME;
+
+@TestClass
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public abstract class ATestPartialHierarchicalMeasureComparison extends ABaseTestQuery {
+
+  private final String storeName = "store" + ATestPartialHierarchicalMeasureComparison.class.getSimpleName().toLowerCase();
+  private final TableField city = new TableField(this.storeName, "city");
+  private final TableField country = new TableField(this.storeName, "country");
+  private final TableField continent = new TableField(this.storeName, "continent");
+  private final TableField spendingCategory = new TableField(this.storeName, "spending_category");
+  private final TableField amount = new TableField(this.storeName, "amount");
+
+  @Override
+  protected Map<String, List<TableTypedField>> getFieldsByStore() {
+    TableTypedField city = new TableTypedField(this.storeName, "city", String.class);
+    TableTypedField country = new TableTypedField(this.storeName, "country", String.class);
+    TableTypedField continent = new TableTypedField(this.storeName, "continent", String.class);
+    TableTypedField spendingCategory = new TableTypedField(this.storeName, "spending_category", String.class);
+    TableTypedField amount = new TableTypedField(this.storeName, "amount", double.class);
+    return Map.of(this.storeName, List.of(city, country, continent, spendingCategory, amount));
+  }
+
+  @Override
+  protected void loadData() {
+    this.tm.load(MAIN_SCENARIO_NAME, this.storeName, List.of(
+            new Object[]{"paris", "france", "eu", "car", 2d},
+            new Object[]{"paris", "france", "eu", "home", 4d},
+            new Object[]{"paris", "france", "eu", "hobbies", 1d},
+            new Object[]{"lyon", "france", "eu", "car", 1d},
+            new Object[]{"lyon", "france", "eu", "home", 2d},
+            new Object[]{"lyon", "france", "eu", "hobbies", 1d},
+            new Object[]{"london", "uk", "eu", "car", 2d},
+            new Object[]{"london", "uk", "eu", "home", 5d},
+            new Object[]{"london", "uk", "eu", "hobbies", 3d}
+    ));
+  }
+
+  @Test
+  void testPercentOfParentOfAxis() {
+    Measure pop = Functions.sum("amount", this.amount);
+    List<Field> fields = List.of(this.continent, this.country, this.city);
+    Measure pOp = comparisonMeasureWithParentOfAxis("percentOfParent", DIVIDE, pop, COLUMN);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(fields, List.of(pop, pOp))
+            .rollup(fields)
+            .build();
+
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result).containsExactly(
+            Arrays.asList(GRAND_TOTAL, GRAND_TOTAL, GRAND_TOTAL, 21d, 1d),
+            Arrays.asList("eu", TOTAL, TOTAL, 21d, 21d / 21d),
+            Arrays.asList("eu", "france", TOTAL, 11d, 11d / 21d),
+            Arrays.asList("eu", "france", "lyon", 4d, 4d / 11d),
+            Arrays.asList("eu", "france", "paris", 7d, 7d / 11d),
+            Arrays.asList("eu", "uk", TOTAL, 10d, 10d / 21d),
+            Arrays.asList("eu", "uk", "london", 10d, 10d / 10d));
+  }
+
+  @Test
+  void testPercentOfTotalOfAxis() {
+    Measure pop = Functions.sum("amount", this.amount);
+    List<Field> fields = List.of(this.continent, this.country, this.city);
+    Measure pOp = comparisonMeasureWithTotalOfAxis("percentOfParent", DIVIDE, pop, COLUMN);
+    // Binary measure should work. The underlying measure has to be "transformed".
+    Measure twice = multiply("twice", comparisonMeasureWithTotalOfAxis("percentOfParent", DIVIDE, pop, COLUMN), integer(2));
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(fields, List.of(pop, pOp, twice))
+            .rollup(fields)
+            .build();
+
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result).containsExactly(
+            Arrays.asList(GRAND_TOTAL, GRAND_TOTAL, GRAND_TOTAL, 21d, 1d, 2d),
+            Arrays.asList("eu", TOTAL, TOTAL, 21d, 21d / 21d, 2d),
+            Arrays.asList("eu", "france", TOTAL, 11d, 11d / 21d, 2 * 11d / 21d),
+            Arrays.asList("eu", "france", "lyon", 4d, 4d / 21d, 2 * 4d / 21d),
+            Arrays.asList("eu", "france", "paris", 7d, 7d / 21d, 2 * 7d / 21d),
+            Arrays.asList("eu", "uk", TOTAL, 10d, 10d / 21d, 2 * 10d / 21d),
+            Arrays.asList("eu", "uk", "london", 10d, 10d / 21d, 2 * 10d / 21d));
+  }
+
+  @Test
+  void testCompareWithParentOfColumnPivotTable(TestInfo testInfo) {
+    Measure pop = Functions.sum("amount", this.amount);
+    List<Field> fields = List.of(this.continent, this.country, this.city);
+    Measure pOp = comparisonMeasureWithParentOfAxis("diff", ABSOLUTE_DIFFERENCE, pop, COLUMN);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(FastList.newList(fields).with(this.spendingCategory), List.of(pop, pOp))
+            .build();
+
+    PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(query, fields, List.of(this.spendingCategory)));
+    verifyResults(testInfo, result);
+  }
+
+  @Test
+  void testCompareWithTotalOfColumnPivotTable(TestInfo testInfo) {
+    Measure pop = Functions.sum("amount", this.amount);
+    List<Field> fields = List.of(this.continent, this.country, this.city);
+    Measure pOp = comparisonMeasureWithTotalOfAxis("diff", ABSOLUTE_DIFFERENCE, pop, COLUMN);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(FastList.newList(fields).with(this.spendingCategory), List.of(pop, pOp))
+            .build();
+
+    PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(query, fields, List.of(this.spendingCategory)));
+    verifyResults(testInfo, result);
+  }
+
+  @Test
+  void testCompareWithTotalOfRowPivotTable(TestInfo testInfo) {
+    Measure pop = Functions.sum("amount", this.amount);
+    List<Field> fields = List.of(this.continent, this.country, this.city);
+    Measure pOp = comparisonMeasureWithTotalOfAxis("diff", ABSOLUTE_DIFFERENCE, pop, ROW);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(FastList.newList(fields).with(this.spendingCategory), List.of(pop, pOp))
+            .build();
+
+    PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(query, fields, List.of(this.spendingCategory)));
+    verifyResults(testInfo, result);
+  }
+
+  @Test
+  void testCompareWithParentOfRowPivotTable(TestInfo testInfo) {
+    Measure pop = Functions.sum("amount", this.amount);
+    List<Field> fields = List.of(this.continent, this.country, this.city);
+    Measure pOp = comparisonMeasureWithParentOfAxis("diff", ABSOLUTE_DIFFERENCE, pop, ROW);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(FastList.newList(fields).with(this.spendingCategory), List.of(pop, pOp))
+            .build();
+
+    PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(query, fields, List.of(this.spendingCategory)));
+    verifyResults(testInfo, result);
+  }
+
+  private void verifyResults(TestInfo testInfo, PivotTable pt) {
+    TestUtil.verifyResults("partialmeasurecomparison", testInfo, pt);
+  }
+}
diff --git a/core/src/test/java/io/squashql/query/ATestPivotTable.java b/core/src/test/java/io/squashql/query/ATestPivotTable.java
index 1b16d82..69e94ab 100644
--- a/core/src/test/java/io/squashql/query/ATestPivotTable.java
+++ b/core/src/test/java/io/squashql/query/ATestPivotTable.java
@@ -1,6 +1,5 @@
 package io.squashql.query;
 
-import com.google.common.collect.ImmutableList;
 import io.squashql.TestClass;
 import io.squashql.jackson.JacksonUtil;
 import io.squashql.query.builder.Query;
@@ -15,7 +14,6 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInfo;
 import org.junit.jupiter.api.TestInstance;
 
-import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -100,14 +98,6 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
     ));
   }
 
-  private static Table tableFromFile(TestInfo testInfo) {
-    return TestUtil.deserializeTableFromFile(Paths.get("queryresults", "pivottable", testInfo.getTestMethod().get().getName() + ".tabular.json"));
-  }
-
-  private static List<List<Object>> pivotTableFromFile(TestInfo testInfo) {
-    return TestUtil.deserializeFromFile(Paths.get("queryresults", "pivottable", testInfo.getTestMethod().get().getName() + ".pivottable.json"), List.class);
-  }
-
   @Test
   void testRollupEquivalent() {
     Measure amount = Functions.sum("amount", "amount");
@@ -892,17 +882,7 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
     verifyResults(testInfo, pt);
   }
 
-  /**
-   * To save in file '*.tabular.json': System.out.println(TestUtil.tableToJson(pivotTable.table));
-   * To save in file '*.pivottable.json': System.out.println(JacksonUtil.serialize(pivotTable.pivotTableCells));
-   */
   private void verifyResults(TestInfo testInfo, PivotTable pt) {
-    Table expectedTabular = tableFromFile(testInfo);
-
-    Assertions.assertThat(pt.table).containsExactlyElementsOf(ImmutableList.copyOf(expectedTabular.iterator()));
-    Assertions.assertThat(pt.table.headers()).containsExactlyElementsOf(expectedTabular.headers());
-
-    List<List<Object>> expectedPivotTable = pivotTableFromFile(testInfo);
-    Assertions.assertThat(pt.pivotTableCells).containsExactlyElementsOf(expectedPivotTable);
+    TestUtil.verifyResults("pivottable", testInfo, pt);
   }
 }
diff --git a/core/src/test/java/io/squashql/query/ATestQueryCache.java b/core/src/test/java/io/squashql/query/ATestQueryCache.java
index 4823b8e..9ba03fe 100644
--- a/core/src/test/java/io/squashql/query/ATestQueryCache.java
+++ b/core/src/test/java/io/squashql/query/ATestQueryCache.java
@@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Supplier;
 
 import static io.squashql.query.Functions.*;
+import static io.squashql.query.QueryExecutor.createPivotTableContext;
 import static io.squashql.query.TableField.tableField;
 import static io.squashql.query.TableField.tableFields;
 import static io.squashql.query.database.QueryEngine.GRAND_TOTAL;
@@ -610,6 +611,7 @@ public abstract class ATestQueryCache extends ABaseTestQuery {
             CacheStatsDto.builder(),
             user,
             true,
-            null);
+            null,
+            createPivotTableContext(query));
   }
 }
diff --git a/core/src/test/java/io/squashql/query/ATestVectorOperation.java b/core/src/test/java/io/squashql/query/ATestVectorOperation.java
index 627c606..4c41d8e 100644
--- a/core/src/test/java/io/squashql/query/ATestVectorOperation.java
+++ b/core/src/test/java/io/squashql/query/ATestVectorOperation.java
@@ -145,6 +145,38 @@ public abstract class ATestVectorOperation extends ABaseTestQuery {
             List.of(competitorZ, productB, -1d));
   }
 
+
+  @Test
+  void testPartialIncrementalVar() {
+    List<Field> fields = List.of(this.competitor, this.ean);
+
+    Measure incrementalVar = new ParametrizedMeasure("partial inc var",
+            Repository.INCREMENTAL_VAR, Map.of(
+            "value", this.price,
+            "date", this.date,
+            "quantile", 0.95,
+            "axis", Axis.COLUMN
+    ));
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(fields, List.of(incrementalVar))
+            .rollup(fields)
+            .build();
+    Table result = this.executor.executeQuery(query);
+    // Result: same as above.
+    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/core/src/test/java/io/squashql/util/TestUtil.java b/core/src/test/java/io/squashql/util/TestUtil.java
index 5fe04f8..8bfa81c 100644
--- a/core/src/test/java/io/squashql/util/TestUtil.java
+++ b/core/src/test/java/io/squashql/util/TestUtil.java
@@ -2,11 +2,14 @@ package io.squashql.util;
 
 import com.google.common.collect.ImmutableList;
 import io.squashql.jackson.JacksonUtil;
-import io.squashql.query.*;
+import io.squashql.query.GlobalCache;
+import io.squashql.query.Header;
+import io.squashql.query.SquashQLUser;
 import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.dto.CacheStatsDto;
 import io.squashql.query.dto.SimpleTableDto;
 import io.squashql.table.ColumnarTable;
+import io.squashql.table.PivotTable;
 import io.squashql.table.RowTable;
 import io.squashql.table.Table;
 import lombok.AllArgsConstructor;
@@ -14,6 +17,7 @@ import lombok.NoArgsConstructor;
 import org.assertj.core.api.Assertions;
 import org.assertj.core.api.ThrowableAssert;
 import org.assertj.core.util.Throwables;
+import org.junit.jupiter.api.TestInfo;
 
 import java.io.*;
 import java.nio.file.Path;
@@ -117,6 +121,53 @@ public class TestUtil {
     return new ColumnarTable(headers, measures, values);
   }
 
+  /**
+   * To save in file '*.tabular.json': System.out.println(TestUtil.tableToJson(pivotTable.table));
+   * To save in file '*.pivottable.json': System.out.println(JacksonUtil.serialize(pivotTable.pivotTableCells));
+   *
+   * {@link #writePivotTableToFiles(String, TestInfo, PivotTable)}
+   * TestUtil.writePivotTableToFiles("/Users/paul/dev/github/squashql/core/src/test/resources/queryresults/partialmeasurecomparison", testInfo, pivotTable);
+   */
+  public static void verifyResults(String directory, TestInfo testInfo, PivotTable pt) {
+    Table expectedTabular = tableFromFile(directory, testInfo);
+
+    Assertions.assertThat(pt.table).containsExactlyElementsOf(ImmutableList.copyOf(expectedTabular.iterator()));
+    Assertions.assertThat(pt.table.headers()).containsExactlyElementsOf(expectedTabular.headers());
+
+    List<List<Object>> expectedPivotTable = pivotTableFromFile(directory, testInfo);
+    Assertions.assertThat(pt.pivotTableCells).containsExactlyElementsOf(expectedPivotTable);
+  }
+
+  private static Table tableFromFile(String directory, TestInfo testInfo) {
+    return TestUtil.deserializeTableFromFile(Paths.get("queryresults", directory, testInfo.getTestMethod().get().getName() + ".tabular.json"));
+  }
+
+  private static List<List<Object>> pivotTableFromFile(String directory, TestInfo testInfo) {
+    return TestUtil.deserializeFromFile(Paths.get("queryresults", directory, testInfo.getTestMethod().get().getName() + ".pivottable.json"), List.class);
+  }
+
+  public static void writePivotTableToFiles(String absolutePathDirectory, TestInfo testInfo, PivotTable pt) {
+    try {
+      String fileNameTabular = testInfo.getTestMethod().get().getName() + ".tabular.json";
+      File file = Path.of(absolutePathDirectory, fileNameTabular).toFile();
+      file.createNewFile();
+      PrintWriter writer = new PrintWriter(file);
+      writer.println(TestUtil.tableToJson(pt.table));
+      writer.flush();
+      writer.close();
+
+      String fileNamePivotTable = testInfo.getTestMethod().get().getName() + ".pivottable.json";
+      file = Path.of(absolutePathDirectory, fileNamePivotTable).toFile();
+      file.createNewFile();
+      writer = new PrintWriter(file);
+      writer.println(JacksonUtil.serialize(pt.pivotTableCells));
+      writer.flush();
+      writer.close();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   /**
    * Do not work if the query contains totals or sub-totals.
    */
diff --git a/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfColumnPivotTable.pivottable.json b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfColumnPivotTable.pivottable.json
new file mode 100644
index 0000000..c3f877c
--- /dev/null
+++ b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfColumnPivotTable.pivottable.json
@@ -0,0 +1,1 @@
+[["storeatestpartialhierarchicalmeasurecomparison.spending_category","storeatestpartialhierarchicalmeasurecomparison.spending_category","storeatestpartialhierarchicalmeasurecomparison.spending_category","Grand Total","Grand Total","car","car","hobbies","hobbies","home","home"],["storeatestpartialhierarchicalmeasurecomparison.continent","storeatestpartialhierarchicalmeasurecomparison.country","storeatestpartialhierarchicalmeasurecomparison.city","amount","diff","amount","diff","amount","diff","amount","diff"],["Grand Total","Grand Total","Grand Total",21.0,0.0,5.0,0.0,5.0,0.0,11.0,0.0],["eu","Total","Total",21.0,0.0,5.0,0.0,5.0,0.0,11.0,0.0],["eu","france","Total",11.0,-10.0,3.0,-2.0,2.0,-3.0,6.0,-5.0],["eu","france","lyon",4.0,-7.0,1.0,-2.0,1.0,-1.0,2.0,-4.0],["eu","france","paris",7.0,-4.0,2.0,-1.0,1.0,-1.0,4.0,-2.0],["eu","uk","Total",10.0,-11.0,2.0,-3.0,3.0,-2.0,5.0,-6.0],["eu","uk","london",10.0,0.0,2.0,0.0,3.0,0.0,5.0,0.0]]
diff --git a/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfColumnPivotTable.tabular.json b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfColumnPivotTable.tabular.json
new file mode 100644
index 0000000..5223bc6
--- /dev/null
+++ b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfColumnPivotTable.tabular.json
@@ -0,0 +1,1 @@
+{"rows":[["Grand Total","Grand Total","Grand Total","Grand Total",21.0,0.0],["Grand Total","Grand Total","Grand Total","car",5.0,0.0],["Grand Total","Grand Total","Grand Total","hobbies",5.0,0.0],["Grand Total","Grand Total","Grand Total","home",11.0,0.0],["eu","Total","Total","Grand Total",21.0,0.0],["eu","Total","Total","car",5.0,0.0],["eu","Total","Total","hobbies",5.0,0.0],["eu","Total","Total","home",11.0,0.0],["eu","france","Total","Grand Total",11.0,-10.0],["eu","france","Total","car",3.0,-2.0],["eu","france","Total","hobbies",2.0,-3.0],["eu","france","Total","home",6.0,-5.0],["eu","france","lyon","Grand Total",4.0,-7.0],["eu","france","lyon","car",1.0,-2.0],["eu","france","lyon","hobbies",1.0,-1.0],["eu","france","lyon","home",2.0,-4.0],["eu","france","paris","Grand Total",7.0,-4.0],["eu","france","paris","car",2.0,-1.0],["eu","france","paris","hobbies",1.0,-1.0],["eu","france","paris","home",4.0,-2.0],["eu","uk","Total","Grand Total",10.0,-11.0],["eu","uk","Total","car",2.0,-3.0],["eu","uk","Total","hobbies",3.0,-2.0],["eu","uk","Total","home",5.0,-6.0],["eu","uk","london","Grand Total",10.0,0.0],["eu","uk","london","car",2.0,0.0],["eu","uk","london","hobbies",3.0,0.0],["eu","uk","london","home",5.0,0.0]],"headers":[{"name":"storeatestpartialhierarchicalmeasurecomparison.continent","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.country","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.city","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.spending_category","type":"java.lang.String","isMeasure":false},{"name":"amount","type":"double","isMeasure":true},{"name":"diff","type":"double","isMeasure":true}]}
diff --git a/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfRowPivotTable.pivottable.json b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfRowPivotTable.pivottable.json
new file mode 100644
index 0000000..f596434
--- /dev/null
+++ b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfRowPivotTable.pivottable.json
@@ -0,0 +1,1 @@
+[["storeatestpartialhierarchicalmeasurecomparison.spending_category","storeatestpartialhierarchicalmeasurecomparison.spending_category","storeatestpartialhierarchicalmeasurecomparison.spending_category","Grand Total","Grand Total","car","car","hobbies","hobbies","home","home"],["storeatestpartialhierarchicalmeasurecomparison.continent","storeatestpartialhierarchicalmeasurecomparison.country","storeatestpartialhierarchicalmeasurecomparison.city","amount","diff","amount","diff","amount","diff","amount","diff"],["Grand Total","Grand Total","Grand Total",21.0,0.0,5.0,-16.0,5.0,-16.0,11.0,-10.0],["eu","Total","Total",21.0,0.0,5.0,-16.0,5.0,-16.0,11.0,-10.0],["eu","france","Total",11.0,0.0,3.0,-8.0,2.0,-9.0,6.0,-5.0],["eu","france","lyon",4.0,0.0,1.0,-3.0,1.0,-3.0,2.0,-2.0],["eu","france","paris",7.0,0.0,2.0,-5.0,1.0,-6.0,4.0,-3.0],["eu","uk","Total",10.0,0.0,2.0,-8.0,3.0,-7.0,5.0,-5.0],["eu","uk","london",10.0,0.0,2.0,-8.0,3.0,-7.0,5.0,-5.0]]
diff --git a/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfRowPivotTable.tabular.json b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfRowPivotTable.tabular.json
new file mode 100644
index 0000000..4cbb3d1
--- /dev/null
+++ b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithParentOfRowPivotTable.tabular.json
@@ -0,0 +1,1 @@
+{"rows":[["Grand Total","Grand Total","Grand Total","Grand Total",21.0,0.0],["Grand Total","Grand Total","Grand Total","car",5.0,-16.0],["Grand Total","Grand Total","Grand Total","hobbies",5.0,-16.0],["Grand Total","Grand Total","Grand Total","home",11.0,-10.0],["eu","Total","Total","Grand Total",21.0,0.0],["eu","Total","Total","car",5.0,-16.0],["eu","Total","Total","hobbies",5.0,-16.0],["eu","Total","Total","home",11.0,-10.0],["eu","france","Total","Grand Total",11.0,0.0],["eu","france","Total","car",3.0,-8.0],["eu","france","Total","hobbies",2.0,-9.0],["eu","france","Total","home",6.0,-5.0],["eu","france","lyon","Grand Total",4.0,0.0],["eu","france","lyon","car",1.0,-3.0],["eu","france","lyon","hobbies",1.0,-3.0],["eu","france","lyon","home",2.0,-2.0],["eu","france","paris","Grand Total",7.0,0.0],["eu","france","paris","car",2.0,-5.0],["eu","france","paris","hobbies",1.0,-6.0],["eu","france","paris","home",4.0,-3.0],["eu","uk","Total","Grand Total",10.0,0.0],["eu","uk","Total","car",2.0,-8.0],["eu","uk","Total","hobbies",3.0,-7.0],["eu","uk","Total","home",5.0,-5.0],["eu","uk","london","Grand Total",10.0,0.0],["eu","uk","london","car",2.0,-8.0],["eu","uk","london","hobbies",3.0,-7.0],["eu","uk","london","home",5.0,-5.0]],"headers":[{"name":"storeatestpartialhierarchicalmeasurecomparison.continent","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.country","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.city","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.spending_category","type":"java.lang.String","isMeasure":false},{"name":"amount","type":"double","isMeasure":true},{"name":"diff","type":"double","isMeasure":true}]}
diff --git a/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfColumnPivotTable.pivottable.json b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfColumnPivotTable.pivottable.json
new file mode 100644
index 0000000..74542fb
--- /dev/null
+++ b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfColumnPivotTable.pivottable.json
@@ -0,0 +1,1 @@
+[["storeatestpartialhierarchicalmeasurecomparison.spending_category","storeatestpartialhierarchicalmeasurecomparison.spending_category","storeatestpartialhierarchicalmeasurecomparison.spending_category","Grand Total","Grand Total","car","car","hobbies","hobbies","home","home"],["storeatestpartialhierarchicalmeasurecomparison.continent","storeatestpartialhierarchicalmeasurecomparison.country","storeatestpartialhierarchicalmeasurecomparison.city","amount","diff","amount","diff","amount","diff","amount","diff"],["Grand Total","Grand Total","Grand Total",21.0,0.0,5.0,0.0,5.0,0.0,11.0,0.0],["eu","Total","Total",21.0,0.0,5.0,0.0,5.0,0.0,11.0,0.0],["eu","france","Total",11.0,-10.0,3.0,-2.0,2.0,-3.0,6.0,-5.0],["eu","france","lyon",4.0,-17.0,1.0,-4.0,1.0,-4.0,2.0,-9.0],["eu","france","paris",7.0,-14.0,2.0,-3.0,1.0,-4.0,4.0,-7.0],["eu","uk","Total",10.0,-11.0,2.0,-3.0,3.0,-2.0,5.0,-6.0],["eu","uk","london",10.0,-11.0,2.0,-3.0,3.0,-2.0,5.0,-6.0]]
diff --git a/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfColumnPivotTable.tabular.json b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfColumnPivotTable.tabular.json
new file mode 100644
index 0000000..29e9655
--- /dev/null
+++ b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfColumnPivotTable.tabular.json
@@ -0,0 +1,1 @@
+{"rows":[["Grand Total","Grand Total","Grand Total","Grand Total",21.0,0.0],["Grand Total","Grand Total","Grand Total","car",5.0,0.0],["Grand Total","Grand Total","Grand Total","hobbies",5.0,0.0],["Grand Total","Grand Total","Grand Total","home",11.0,0.0],["eu","Total","Total","Grand Total",21.0,0.0],["eu","Total","Total","car",5.0,0.0],["eu","Total","Total","hobbies",5.0,0.0],["eu","Total","Total","home",11.0,0.0],["eu","france","Total","Grand Total",11.0,-10.0],["eu","france","Total","car",3.0,-2.0],["eu","france","Total","hobbies",2.0,-3.0],["eu","france","Total","home",6.0,-5.0],["eu","france","lyon","Grand Total",4.0,-17.0],["eu","france","lyon","car",1.0,-4.0],["eu","france","lyon","hobbies",1.0,-4.0],["eu","france","lyon","home",2.0,-9.0],["eu","france","paris","Grand Total",7.0,-14.0],["eu","france","paris","car",2.0,-3.0],["eu","france","paris","hobbies",1.0,-4.0],["eu","france","paris","home",4.0,-7.0],["eu","uk","Total","Grand Total",10.0,-11.0],["eu","uk","Total","car",2.0,-3.0],["eu","uk","Total","hobbies",3.0,-2.0],["eu","uk","Total","home",5.0,-6.0],["eu","uk","london","Grand Total",10.0,-11.0],["eu","uk","london","car",2.0,-3.0],["eu","uk","london","hobbies",3.0,-2.0],["eu","uk","london","home",5.0,-6.0]],"headers":[{"name":"storeatestpartialhierarchicalmeasurecomparison.continent","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.country","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.city","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.spending_category","type":"java.lang.String","isMeasure":false},{"name":"amount","type":"double","isMeasure":true},{"name":"diff","type":"double","isMeasure":true}]}
diff --git a/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfRowPivotTable.pivottable.json b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfRowPivotTable.pivottable.json
new file mode 100644
index 0000000..f596434
--- /dev/null
+++ b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfRowPivotTable.pivottable.json
@@ -0,0 +1,1 @@
+[["storeatestpartialhierarchicalmeasurecomparison.spending_category","storeatestpartialhierarchicalmeasurecomparison.spending_category","storeatestpartialhierarchicalmeasurecomparison.spending_category","Grand Total","Grand Total","car","car","hobbies","hobbies","home","home"],["storeatestpartialhierarchicalmeasurecomparison.continent","storeatestpartialhierarchicalmeasurecomparison.country","storeatestpartialhierarchicalmeasurecomparison.city","amount","diff","amount","diff","amount","diff","amount","diff"],["Grand Total","Grand Total","Grand Total",21.0,0.0,5.0,-16.0,5.0,-16.0,11.0,-10.0],["eu","Total","Total",21.0,0.0,5.0,-16.0,5.0,-16.0,11.0,-10.0],["eu","france","Total",11.0,0.0,3.0,-8.0,2.0,-9.0,6.0,-5.0],["eu","france","lyon",4.0,0.0,1.0,-3.0,1.0,-3.0,2.0,-2.0],["eu","france","paris",7.0,0.0,2.0,-5.0,1.0,-6.0,4.0,-3.0],["eu","uk","Total",10.0,0.0,2.0,-8.0,3.0,-7.0,5.0,-5.0],["eu","uk","london",10.0,0.0,2.0,-8.0,3.0,-7.0,5.0,-5.0]]
diff --git a/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfRowPivotTable.tabular.json b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfRowPivotTable.tabular.json
new file mode 100644
index 0000000..4cbb3d1
--- /dev/null
+++ b/core/src/test/resources/queryresults/partialmeasurecomparison/testCompareWithTotalOfRowPivotTable.tabular.json
@@ -0,0 +1,1 @@
+{"rows":[["Grand Total","Grand Total","Grand Total","Grand Total",21.0,0.0],["Grand Total","Grand Total","Grand Total","car",5.0,-16.0],["Grand Total","Grand Total","Grand Total","hobbies",5.0,-16.0],["Grand Total","Grand Total","Grand Total","home",11.0,-10.0],["eu","Total","Total","Grand Total",21.0,0.0],["eu","Total","Total","car",5.0,-16.0],["eu","Total","Total","hobbies",5.0,-16.0],["eu","Total","Total","home",11.0,-10.0],["eu","france","Total","Grand Total",11.0,0.0],["eu","france","Total","car",3.0,-8.0],["eu","france","Total","hobbies",2.0,-9.0],["eu","france","Total","home",6.0,-5.0],["eu","france","lyon","Grand Total",4.0,0.0],["eu","france","lyon","car",1.0,-3.0],["eu","france","lyon","hobbies",1.0,-3.0],["eu","france","lyon","home",2.0,-2.0],["eu","france","paris","Grand Total",7.0,0.0],["eu","france","paris","car",2.0,-5.0],["eu","france","paris","hobbies",1.0,-6.0],["eu","france","paris","home",4.0,-3.0],["eu","uk","Total","Grand Total",10.0,0.0],["eu","uk","Total","car",2.0,-8.0],["eu","uk","Total","hobbies",3.0,-7.0],["eu","uk","Total","home",5.0,-5.0],["eu","uk","london","Grand Total",10.0,0.0],["eu","uk","london","car",2.0,-8.0],["eu","uk","london","hobbies",3.0,-7.0],["eu","uk","london","home",5.0,-5.0]],"headers":[{"name":"storeatestpartialhierarchicalmeasurecomparison.continent","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.country","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.city","type":"java.lang.String","isMeasure":false},{"name":"storeatestpartialhierarchicalmeasurecomparison.spending_category","type":"java.lang.String","isMeasure":false},{"name":"amount","type":"double","isMeasure":true},{"name":"diff","type":"double","isMeasure":true}]}
diff --git a/duckdb/src/test/java/io/squashql/TestDuckDBQuery.java b/duckdb/src/test/java/io/squashql/TestDuckDBQuery.java
index 57cf58b..f35eb6b 100644
--- a/duckdb/src/test/java/io/squashql/TestDuckDBQuery.java
+++ b/duckdb/src/test/java/io/squashql/TestDuckDBQuery.java
@@ -5,6 +5,7 @@ import io.squashql.query.builder.Query;
 import io.squashql.query.database.DuckDBQueryEngine;
 import io.squashql.query.dto.*;
 import io.squashql.query.exception.LimitExceedException;
+import io.squashql.query.measure.ParametrizedMeasure;
 import io.squashql.table.Table;
 import io.squashql.transaction.DuckDBDataLoader;
 import io.squashql.type.TableTypedField;
@@ -12,12 +13,15 @@ import io.squashql.util.TestUtil;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.time.LocalDate;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static io.squashql.query.Functions.sum;
+import static io.squashql.query.QueryExecutor.createPivotTableContext;
 import static io.squashql.query.TableField.tableField;
 import static io.squashql.query.TableField.tableFields;
+import static io.squashql.query.measure.Repository.VAR;
 
 public class TestDuckDBQuery {
   protected String storeName = "myStore";
@@ -64,12 +68,12 @@ public class TestDuckDBQuery {
             .build();
 
     AtomicInteger limitCapture = new AtomicInteger(-1);
-    this.executor.executeQuery(query, CacheStatsDto.builder(), null, true, limitCapture::set);
+    this.executor.executeQuery(query, CacheStatsDto.builder(), null, true, limitCapture::set, createPivotTableContext(query));
     Assertions.assertThat(limitCapture.get()).isEqualTo(-1);
 
     int limit = 2;
     query.limit = limit;
-    this.executor.executeQuery(query, CacheStatsDto.builder(), null, true, limitCapture::set);
+    this.executor.executeQuery(query, CacheStatsDto.builder(), null, true, limitCapture::set, createPivotTableContext(query));
     Assertions.assertThat(limitCapture.get()).isEqualTo(limit);
   }
 
@@ -216,4 +220,26 @@ public class TestDuckDBQuery {
             List.of("Food & Drink", "drink", 2023, "drink", 2023, 2d),
             List.of("Other", "cloth", 2023, "cloth", 2023, 10d));
   }
+
+  @Test
+  void testMissingParameterInParametrizedMeasure() {
+    TableTypedField date = new TableTypedField(this.storeName, "date", LocalDate.class);
+    setup(Map.of(this.storeName, List.of(date)), () -> {
+    });
+    Measure var = new ParametrizedMeasure(
+            "var 95",
+            VAR,
+            Map.of(
+                    // value is missing
+                    "date", date,
+                    "quantile", 0.95
+            ));
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(Collections.emptyList(), List.of(var))
+            .build();
+    Assertions.assertThatThrownBy(() -> this.executor.executeQuery(query))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessageContaining("Parameter 'value' was expected but not provided");
+  }
 }
diff --git a/duckdb/src/test/java/io/squashql/query/TestDuckDBPartialHierarchicalMeasureComparison.java b/duckdb/src/test/java/io/squashql/query/TestDuckDBPartialHierarchicalMeasureComparison.java
new file mode 100644
index 0000000..749b45f
--- /dev/null
+++ b/duckdb/src/test/java/io/squashql/query/TestDuckDBPartialHierarchicalMeasureComparison.java
@@ -0,0 +1,35 @@
+package io.squashql.query;
+
+import io.squashql.DuckDBDatastore;
+import io.squashql.query.database.DuckDBQueryEngine;
+import io.squashql.query.database.QueryEngine;
+import io.squashql.store.Datastore;
+import io.squashql.transaction.DataLoader;
+import io.squashql.transaction.DuckDBDataLoader;
+
+/**
+ * Do not edit this class, it has been generated automatically by {@link io.squashql.template.DuckDBClassTemplateGenerator}.
+ */
+public class TestDuckDBPartialHierarchicalMeasureComparison extends ATestPartialHierarchicalMeasureComparison {
+
+  @Override
+  protected QueryEngine createQueryEngine(Datastore datastore) {
+    return new DuckDBQueryEngine((DuckDBDatastore) datastore);
+  }
+
+  @Override
+  protected Datastore createDatastore() {
+    return new DuckDBDatastore();
+  }
+
+  @Override
+  protected DataLoader createDataLoader() {
+    return new DuckDBDataLoader((DuckDBDatastore) this.datastore);
+  }
+
+  @Override
+  protected void createTables() {
+    DuckDBDataLoader tm = (DuckDBDataLoader) this.tm;
+    this.fieldsByStore.forEach(tm::createOrReplaceTable);
+  }
+}
diff --git a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
index 455b7da..08cefa5 100644
--- a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
+++ b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
@@ -99,6 +99,18 @@ public class TestJavascriptLibrary {
             "quantile", 0.95,
             "ancestors", tableFields(List.of("f1", "f2", "f3"))
     )));
+    q.withMeasure(comparisonMeasureWithParentOfAxis(
+            "comp parent of column",
+            ComparisonMethod.DIVIDE,
+            CountMeasure.INSTANCE,
+            Axis.COLUMN
+    ));
+    q.withMeasure(comparisonMeasureWithTotalOfAxis(
+            "comp total of row",
+            ComparisonMethod.DIVIDE,
+            CountMeasure.INSTANCE,
+            Axis.ROW
+    ));
 
     var queryCondition = or(and(eq("a"), eq("b")), lt(5), like("a%"));
     q.withCondition(tableField("f1"), queryCondition);
diff --git a/snowflake/src/test/java/io/squashql/query/TestSnowflakePartialHierarchicalMeasureComparison.java b/snowflake/src/test/java/io/squashql/query/TestSnowflakePartialHierarchicalMeasureComparison.java
new file mode 100644
index 0000000..64331a2
--- /dev/null
+++ b/snowflake/src/test/java/io/squashql/query/TestSnowflakePartialHierarchicalMeasureComparison.java
@@ -0,0 +1,52 @@
+package io.squashql.query;
+
+import io.squashql.SnowflakeDatastore;
+import io.squashql.query.database.QueryEngine;
+import io.squashql.query.database.SnowflakeQueryEngine;
+import io.squashql.store.Datastore;
+import io.squashql.transaction.DataLoader;
+import io.squashql.transaction.SnowflakeDataLoader;
+import org.junit.jupiter.api.AfterAll;
+
+/**
+ * Do not edit this class, it has been generated automatically by {@link io.squashql.template.SnowflakeClassTemplateGenerator}.
+ */
+public class TestSnowflakePartialHierarchicalMeasureComparison extends ATestPartialHierarchicalMeasureComparison {
+
+  @AfterAll
+  void tearDown() {
+    SnowflakeDataLoader tm = (SnowflakeDataLoader) this.tm;
+    this.fieldsByStore.forEach((storeName, storeFields) -> tm.dropTable(storeName));
+  }
+
+  @Override
+  protected void createTables() {
+    SnowflakeDataLoader tm = (SnowflakeDataLoader) this.tm;
+    this.fieldsByStore.forEach(tm::createOrReplaceTable);
+  }
+
+  @Override
+  protected QueryEngine createQueryEngine(Datastore datastore) {
+    return new SnowflakeQueryEngine((SnowflakeDatastore) datastore);
+  }
+
+  @Override
+  protected Datastore createDatastore() {
+    return new SnowflakeDatastore(
+            SnowflakeTestUtil.jdbcUrl,
+            SnowflakeTestUtil.database,
+            SnowflakeTestUtil.schema,
+            SnowflakeTestUtil.properties
+    );
+  }
+
+  @Override
+  protected DataLoader createDataLoader() {
+    return new SnowflakeDataLoader((SnowflakeDatastore) this.datastore);
+  }
+
+  @Override
+  protected Object translate(Object o) {
+    return SnowflakeTestUtil.translate(o);
+  }
+}
diff --git a/spark/src/test/java/io/squashql/query/TestSparkPartialHierarchicalMeasureComparison.java b/spark/src/test/java/io/squashql/query/TestSparkPartialHierarchicalMeasureComparison.java
new file mode 100644
index 0000000..7da6482
--- /dev/null
+++ b/spark/src/test/java/io/squashql/query/TestSparkPartialHierarchicalMeasureComparison.java
@@ -0,0 +1,42 @@
+package io.squashql.query;
+
+import io.squashql.SparkDatastore;
+import io.squashql.query.database.QueryEngine;
+import io.squashql.query.database.SparkQueryEngine;
+import io.squashql.store.Datastore;
+import io.squashql.transaction.DataLoader;
+import io.squashql.transaction.SparkDataLoader;
+import org.junit.jupiter.api.AfterAll;
+
+/**
+ * Do not edit this class, it has been generated automatically by {@link io.squashql.template.SparkClassTemplateGenerator}.
+ */
+public class TestSparkPartialHierarchicalMeasureComparison extends ATestPartialHierarchicalMeasureComparison {
+
+  @AfterAll
+  void tearDown() {
+    this.fieldsByStore.keySet().forEach(storeName -> ((SparkDatastore) this.datastore).spark.catalog().dropTempView(storeName));
+  }
+
+  @Override
+  protected QueryEngine createQueryEngine(Datastore datastore) {
+    return new SparkQueryEngine((SparkDatastore) datastore);
+  }
+
+  @Override
+  protected Datastore createDatastore() {
+    return new SparkDatastore();
+  }
+
+  @Override
+  protected DataLoader createDataLoader() {
+    SparkDatastore ds = (SparkDatastore) this.datastore;
+    return new SparkDataLoader(ds.spark);
+  }
+
+  @Override
+  protected void createTables() {
+    SparkDataLoader tm = (SparkDataLoader) this.tm;
+    this.fieldsByStore.forEach((store, fields) -> tm.createTemporaryTable(store, fields));
+  }
+}