Repository: squashql/squashql
Pull request #276 (2024-02-19 14:16:02)

Comparison measure Grand Total


Patch:
diff --git a/core/src/main/java/io/squashql/query/AComparisonExecutor.java b/core/src/main/java/io/squashql/query/AComparisonExecutor.java
index bb11241..8ce1399 100644
--- a/core/src/main/java/io/squashql/query/AComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/AComparisonExecutor.java
@@ -17,15 +17,15 @@ import java.util.function.BiPredicate;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-public abstract class AComparisonExecutor {
+public abstract class AComparisonExecutor<T extends CompiledComparisonMeasure> {
 
   public static final String REF_POS_FIRST = "first";
 
-  protected abstract BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasure cm,
+  protected abstract BiPredicate<Object[], Header[]> createShiftProcedure(T cm,
                                                                          ObjectIntMap<String> indexByColumn);
 
   public List<Object> compare(
-          CompiledComparisonMeasure cm,
+          T cm,
           Table writeToTable,
           Table readFromTable) {
     MutableObjectIntMap<String> indexByColumn = new ObjectIntHashMap<>();
diff --git a/core/src/main/java/io/squashql/query/ComparisonMeasureGrandTotal.java b/core/src/main/java/io/squashql/query/ComparisonMeasureGrandTotal.java
new file mode 100644
index 0000000..916f5f1
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/ComparisonMeasureGrandTotal.java
@@ -0,0 +1,36 @@
+package io.squashql.query;
+
+import lombok.*;
+
+@ToString
+@EqualsAndHashCode
+@NoArgsConstructor // For Jackson
+@AllArgsConstructor
+public class ComparisonMeasureGrandTotal implements Measure {
+
+  public String alias;
+  @With
+  public String expression;
+  @Getter
+  public ComparisonMethod comparisonMethod;
+  @Getter
+  public Measure measure;
+
+  public ComparisonMeasureGrandTotal(@NonNull String alias,
+                                     @NonNull ComparisonMethod comparisonMethod,
+                                     @NonNull Measure measure) {
+    this.alias = alias;
+    this.comparisonMethod = comparisonMethod;
+    this.measure = measure;
+  }
+
+  @Override
+  public String alias() {
+    return this.alias;
+  }
+
+  @Override
+  public String expression() {
+    return this.expression;
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/GrandTotalComparisonExecutor.java b/core/src/main/java/io/squashql/query/GrandTotalComparisonExecutor.java
new file mode 100644
index 0000000..921786d
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/GrandTotalComparisonExecutor.java
@@ -0,0 +1,18 @@
+package io.squashql.query;
+
+import io.squashql.query.compiled.CompiledGrandTotalComparisonMeasure;
+import io.squashql.query.database.SQLTranslator;
+import org.eclipse.collections.api.map.primitive.ObjectIntMap;
+
+import java.util.function.BiPredicate;
+
+public class GrandTotalComparisonExecutor extends AComparisonExecutor<CompiledGrandTotalComparisonMeasure> {
+
+  @Override
+  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledGrandTotalComparisonMeasure cm, ObjectIntMap<String> indexByColumn) {
+    return (row, __) -> {
+      indexByColumn.forEachKeyValue((value, index) -> row[index] = SQLTranslator.TOTAL_CELL);
+      return true;
+    };
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/GroupComparisonExecutor.java b/core/src/main/java/io/squashql/query/GroupComparisonExecutor.java
index a43f5d0..a2b7609 100644
--- a/core/src/main/java/io/squashql/query/GroupComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/GroupComparisonExecutor.java
@@ -1,7 +1,7 @@
 package io.squashql.query;
 
 import io.squashql.query.compiled.CompiledGroupColumnSet;
-import io.squashql.query.compiled.CompiledComparisonMeasure;
+import io.squashql.query.compiled.CompiledComparisonMeasureReferencePosition;
 import io.squashql.query.database.SqlUtils;
 import io.squashql.type.TypedField;
 import org.eclipse.collections.api.map.primitive.ObjectIntMap;
@@ -14,16 +14,16 @@ import java.util.List;
 import java.util.Map;
 import java.util.function.BiPredicate;
 
-public class GroupComparisonExecutor extends AComparisonExecutor {
+public class GroupComparisonExecutor extends AComparisonExecutor<CompiledComparisonMeasureReferencePosition> {
 
-  final CompiledGroupColumnSet cSet;
+  private final CompiledGroupColumnSet cSet;
 
   public GroupComparisonExecutor(CompiledGroupColumnSet cSet) {
     this.cSet = cSet;
   }
 
   @Override
-  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasure cm, ObjectIntMap<String> indexByColumn) {
+  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasureReferencePosition cm, ObjectIntMap<String> indexByColumn) {
     return new ShiftProcedure(this.cSet, cm.referencePosition(), indexByColumn);
   }
 
diff --git a/core/src/main/java/io/squashql/query/MeasureUtils.java b/core/src/main/java/io/squashql/query/MeasureUtils.java
index 7de69e4..cef9f7b 100644
--- a/core/src/main/java/io/squashql/query/MeasureUtils.java
+++ b/core/src/main/java/io/squashql/query/MeasureUtils.java
@@ -79,7 +79,7 @@ public final class MeasureUtils {
   public static QueryExecutor.QueryScope getReadScopeComparisonMeasureReferencePosition(
           List<TypedField> columns,
           List<TypedField> groupColumns,
-          CompiledComparisonMeasure cm,
+          CompiledComparisonMeasureReferencePosition cm,
           QueryExecutor.QueryScope queryScope) {
     AtomicReference<CompiledCriteria> copy = new AtomicReference<>(queryScope.whereCriteria() == null ? null : CompiledCriteria.deepCopy(queryScope.whereCriteria()));
     Consumer<TypedField> criteriaRemover = field -> copy.set(removeCriteriaOnField(field, copy.get()));
@@ -103,6 +103,17 @@ public final class MeasureUtils {
             queryScope.limit());
   }
 
+  public static QueryExecutor.QueryScope getReadScopeComparisonGrandTotalMeasure(QueryExecutor.QueryScope queryScope) {
+    return new QueryExecutor.QueryScope(queryScope.table(),
+            queryScope.columns(),
+            queryScope.whereCriteria(),
+            queryScope.havingCriteria(),
+            Collections.emptyList(),
+            List.of(queryScope.columns(), List.of()), // list of empty list => GT
+            queryScope.cteRecordTables(),
+            queryScope.limit());
+  }
+
   private static CompiledCriteria removeCriteriaOnField(TypedField field, CompiledCriteria root) {
     if (root == null) {
       return null;
diff --git a/core/src/main/java/io/squashql/query/ParentComparisonExecutor.java b/core/src/main/java/io/squashql/query/ParentComparisonExecutor.java
index f426d17..4f0dcb8 100644
--- a/core/src/main/java/io/squashql/query/ParentComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/ParentComparisonExecutor.java
@@ -1,6 +1,6 @@
 package io.squashql.query;
 
-import io.squashql.query.compiled.CompiledComparisonMeasure;
+import io.squashql.query.compiled.CompiledComparisonMeasureReferencePosition;
 import io.squashql.query.database.SQLTranslator;
 import io.squashql.query.database.SqlUtils;
 import io.squashql.type.TypedField;
@@ -11,16 +11,16 @@ import java.util.Collections;
 import java.util.List;
 import java.util.function.BiPredicate;
 
-public class ParentComparisonExecutor extends AComparisonExecutor {
+public class ParentComparisonExecutor extends AComparisonExecutor<CompiledComparisonMeasureReferencePosition> {
 
-  final CompiledComparisonMeasure pcm;
+  private final CompiledComparisonMeasureReferencePosition pcm;
 
-  public ParentComparisonExecutor(CompiledComparisonMeasure pcm) {
+  public ParentComparisonExecutor(CompiledComparisonMeasureReferencePosition pcm) {
     this.pcm = pcm;
   }
 
   @Override
-  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasure cm, ObjectIntMap<String> indexByColumn) {
+  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasureReferencePosition cm, ObjectIntMap<String> indexByColumn) {
     List<TypedField> ancestors = new ArrayList<>(this.pcm.ancestors());
     Collections.reverse(ancestors);
     return new AncestorShiftProcedure(ancestors, indexByColumn, cm.grandTotalAlongAncestors());
diff --git a/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java b/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java
index 28a1677..81514ce 100644
--- a/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java
@@ -1,6 +1,6 @@
 package io.squashql.query;
 
-import io.squashql.query.compiled.CompiledComparisonMeasure;
+import io.squashql.query.compiled.CompiledComparisonMeasureReferencePosition;
 import io.squashql.query.compiled.CompiledPeriod;
 import io.squashql.query.database.SQLTranslator;
 import io.squashql.query.database.SqlUtils;
@@ -17,11 +17,11 @@ import java.util.function.BiPredicate;
 
 import static io.squashql.query.PeriodUnit.*;
 
-public class PeriodComparisonExecutor extends AComparisonExecutor {
+public class PeriodComparisonExecutor extends AComparisonExecutor<CompiledComparisonMeasureReferencePosition> {
 
-  final CompiledComparisonMeasure cmrp;
+  private final CompiledComparisonMeasureReferencePosition cmrp;
 
-  public PeriodComparisonExecutor(CompiledComparisonMeasure cmrp) {
+  public PeriodComparisonExecutor(CompiledComparisonMeasureReferencePosition cmrp) {
     this.cmrp = cmrp;
   }
 
@@ -40,7 +40,7 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
   }
 
   @Override
-  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasure cm, ObjectIntMap<String> indexByColumn) {
+  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasureReferencePosition cm, ObjectIntMap<String> indexByColumn) {
     Map<PeriodUnit, String> referencePosition = new HashMap<>();
     CompiledPeriod period = this.cmrp.period();
     Map<TypedField, PeriodUnit> mapping = mapping(period);
@@ -75,10 +75,10 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
       this.indexByPeriodUnit = indexByPeriodUnit;
       PeriodUnit[] periodUnits = getPeriodUnits(period);
       this.transformationByPeriodUnit = new HashMap<>();
-      for (int i = 0; i < periodUnits.length; i++) {
-        Object parse = parse(referencePosition.get(periodUnits[i]));
+      for (PeriodUnit periodUnit : periodUnits) {
+        Object parse = parse(referencePosition.get(periodUnit));
         if (parse != null) {
-          this.transformationByPeriodUnit.put(periodUnits[i], parse);
+          this.transformationByPeriodUnit.put(periodUnit, parse);
         }
       }
     }
@@ -109,7 +109,7 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
             return false;
           }
           LocalDate d = LocalDate.of(readAsLong(row[yearIndex]), quarter * 3, 1);
-          LocalDate newDate = d.plusMonths(((int) quarterTransformation) * 3);
+          LocalDate newDate = d.plusMonths(((int) quarterTransformation) * 3l);
           write(row, quarterIndex, headers[quarterIndex], (int) IsoFields.QUARTER_OF_YEAR.getFrom(newDate));
           write(row, yearIndex, headers[yearIndex], newDate.getYear());// year might have changed
         }
diff --git a/core/src/main/java/io/squashql/query/QueryResolver.java b/core/src/main/java/io/squashql/query/QueryResolver.java
index 492ad7f..bec32a4 100644
--- a/core/src/main/java/io/squashql/query/QueryResolver.java
+++ b/core/src/main/java/io/squashql/query/QueryResolver.java
@@ -298,16 +298,18 @@ public class QueryResolver {
   protected CompiledMeasure compileMeasure(Measure measure, boolean topMeasure) {
     return this.cache.computeIfAbsent(measure, m -> {
       final CompiledMeasure compiledMeasure;
-      if (m instanceof AggregatedMeasure) {
-        compiledMeasure = compileAggregatedMeasure((AggregatedMeasure) m);
-      } else if (m instanceof ExpressionMeasure) {
-        compiledMeasure = compileExpressionMeasure((ExpressionMeasure) m);
-      } else if (m instanceof ConstantMeasure<?>) {
-        compiledMeasure = compileConstantMeasure((ConstantMeasure<?>) m);
-      } else if (m instanceof BinaryOperationMeasure) {
-        compiledMeasure = compileBinaryOperationMeasure((BinaryOperationMeasure) m, topMeasure);
-      } else if (m instanceof ComparisonMeasureReferencePosition) {
-        compiledMeasure = compileComparisonMeasure((ComparisonMeasureReferencePosition) m, topMeasure);
+      if (m instanceof AggregatedMeasure am) {
+        compiledMeasure = compileAggregatedMeasure(am);
+      } else if (m instanceof ExpressionMeasure em) {
+        compiledMeasure = compileExpressionMeasure(em);
+      } else if (m instanceof ConstantMeasure<?> cm) {
+        compiledMeasure = compileConstantMeasure(cm);
+      } else if (m instanceof BinaryOperationMeasure bom) {
+        compiledMeasure = compileBinaryOperationMeasure(bom, topMeasure);
+      } else if (m instanceof ComparisonMeasureReferencePosition cmrp) {
+        compiledMeasure = compileComparisonMeasure(cmrp, topMeasure);
+      } else if (m instanceof ComparisonMeasureGrandTotal cmgt) {
+        compiledMeasure = new CompiledGrandTotalComparisonMeasure(cmgt.alias, cmgt.comparisonMethod, compileMeasure(cmgt.measure, topMeasure));
       } else if (m instanceof VectorAggMeasure v) {
         compiledMeasure = compileVectorAggMeasure(v);
       } else if (m instanceof VectorTupleAggMeasure v) {
@@ -351,7 +353,7 @@ public class QueryResolver {
   }
 
   private CompiledMeasure compileComparisonMeasure(ComparisonMeasureReferencePosition m, boolean topMeasure) {
-    return new CompiledComparisonMeasure(
+    return new CompiledComparisonMeasureReferencePosition(
             m.alias,
             m.comparisonMethod,
             m.comparisonOperator,
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasure.java
index 6c6bf82..751cdcd 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasure.java
@@ -1,37 +1,17 @@
 package io.squashql.query.compiled;
 
-import io.squashql.query.ColumnSetKey;
 import io.squashql.query.ComparisonMethod;
-import io.squashql.query.database.QueryRewriter;
-import io.squashql.type.TypedField;
 
-import java.util.List;
-import java.util.Map;
 import java.util.function.BiFunction;
 
-public record CompiledComparisonMeasure(String alias,
-                                        ComparisonMethod comparisonMethod,
-                                        BiFunction<Object, Object, Object> comparisonOperator,
-                                        CompiledMeasure measure,
-                                        Map<TypedField, String> referencePosition,
-                                        CompiledPeriod period,
-                                        ColumnSetKey columnSetKey,
-                                        List<TypedField> ancestors,
-                                        boolean grandTotalAlongAncestors) implements CompiledMeasure {
+/**
+ * Interface for comparison measure.
+ */
+public interface CompiledComparisonMeasure extends CompiledMeasure {
 
+  CompiledMeasure measure();
 
-  @Override
-  public String sqlExpression(QueryRewriter queryRewriter, boolean withAlias) {
-    throw new IllegalStateException("incorrect path of execution");
-  }
+  ComparisonMethod comparisonMethod();
 
-  @Override
-  public String alias() {
-    return this.alias;
-  }
-
-  @Override
-  public <R> R accept(MeasureVisitor<R> visitor) {
-    return visitor.visit(this);
-  }
+  BiFunction<Object, Object, Object> comparisonOperator();
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java b/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java
new file mode 100644
index 0000000..368458c
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java
@@ -0,0 +1,37 @@
+package io.squashql.query.compiled;
+
+import io.squashql.query.ColumnSetKey;
+import io.squashql.query.ComparisonMethod;
+import io.squashql.query.database.QueryRewriter;
+import io.squashql.type.TypedField;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+public record CompiledComparisonMeasureReferencePosition(String alias,
+                                                         ComparisonMethod comparisonMethod,
+                                                         BiFunction<Object, Object, Object> comparisonOperator,
+                                                         CompiledMeasure measure,
+                                                         Map<TypedField, String> referencePosition,
+                                                         CompiledPeriod period,
+                                                         ColumnSetKey columnSetKey,
+                                                         List<TypedField> ancestors,
+                                                         boolean grandTotalAlongAncestors) implements CompiledComparisonMeasure {
+
+
+  @Override
+  public String sqlExpression(QueryRewriter queryRewriter, boolean withAlias) {
+    throw new IllegalStateException("incorrect path of execution");
+  }
+
+  @Override
+  public String alias() {
+    return this.alias;
+  }
+
+  @Override
+  public <R> R accept(MeasureVisitor<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
new file mode 100644
index 0000000..1e951e2
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledGrandTotalComparisonMeasure.java
@@ -0,0 +1,31 @@
+package io.squashql.query.compiled;
+
+import io.squashql.query.ComparisonMethod;
+import io.squashql.query.database.QueryRewriter;
+
+import java.util.function.BiFunction;
+
+public record CompiledGrandTotalComparisonMeasure(String alias,
+                                                  ComparisonMethod comparisonMethod,
+                                                  CompiledMeasure measure) implements CompiledComparisonMeasure {
+
+  @Override
+  public BiFunction<Object, Object, Object> comparisonOperator() {
+    return null;
+  }
+
+  @Override
+  public String sqlExpression(QueryRewriter queryRewriter, boolean withAlias) {
+    throw new IllegalStateException("incorrect path of execution");
+  }
+
+  @Override
+  public String alias() {
+    return this.alias;
+  }
+
+  @Override
+  public <R> R accept(MeasureVisitor<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 3261066..e1cdfcd 100644
--- a/core/src/main/java/io/squashql/query/compiled/Evaluator.java
+++ b/core/src/main/java/io/squashql/query/compiled/Evaluator.java
@@ -50,8 +50,8 @@ public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>
   }
 
   @Override
-  public Void visit(CompiledComparisonMeasure cm) {
-    AComparisonExecutor executor;
+  public Void visit(CompiledComparisonMeasureReferencePosition cm) {
+    AComparisonExecutor<CompiledComparisonMeasureReferencePosition> executor;
     if (cm.columnSetKey() == GROUP) {
       CompiledColumnSet cs = this.executionContext.columnSets().get(GROUP);
       if (cs == null) {
@@ -81,7 +81,18 @@ public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>
     return null;
   }
 
-  private static void executeComparator(CompiledComparisonMeasure cm, Table writeToTable, Table readFromTable, AComparisonExecutor executor) {
+  @Override
+  public Void visit(CompiledGrandTotalComparisonMeasure cm) {
+    QueryExecutor.QueryScope readScope = MeasureUtils.getReadScopeComparisonGrandTotalMeasure(this.executionContext.queryScope());
+    Table readFromTable = this.executionContext.tableByScope().get(readScope); // Table where to read the aggregates
+    if (readFromTable.count() == this.executionContext.queryLimit()) {
+      throw new RuntimeException("Too many rows, some intermediate results exceed the limit " + this.executionContext.queryLimit());
+    }
+    executeComparator(cm, this.executionContext.getWriteToTable(), readFromTable, new GrandTotalComparisonExecutor());
+    return null;
+  }
+
+  private static <T extends CompiledComparisonMeasure> void executeComparator(T cm, Table writeToTable, Table readFromTable, AComparisonExecutor<T> executor) {
     List<Object> agg = executor.compare(cm, writeToTable, readFromTable);
     Class<?> outputType = cm.comparisonOperator() != null ? UnknownType.class : BinaryOperations.getComparisonOutputType(cm.comparisonMethod(), writeToTable.getHeader(cm.measure()).type());
     Header header = new Header(cm.alias(), outputType, true);
diff --git a/core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java b/core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java
index dd1803c..882f78f 100644
--- a/core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java
+++ b/core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java
@@ -8,7 +8,9 @@ public interface MeasureVisitor<R> {
 
   R visit(CompiledBinaryOperationMeasure measure);
 
-  R visit(CompiledComparisonMeasure measure);
+  R visit(CompiledComparisonMeasureReferencePosition measure);
+
+  R visit(CompiledGrandTotalComparisonMeasure compiledGrandTotalComparisonMeasure);
 
   R visit(CompiledDoubleConstantMeasure measure);
 
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 b1ba3df..a3a793b 100644
--- a/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java
+++ b/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java
@@ -45,13 +45,21 @@ public class PrefetchVisitor implements MeasureVisitor<Map<QueryScope, Set<Compi
   }
 
   @Override
-  public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledComparisonMeasure cmrp) {
+  public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledComparisonMeasureReferencePosition cmrp) {
     QueryScope readScope = MeasureUtils.getReadScopeComparisonMeasureReferencePosition(this.columns, this.groupColumns, cmrp, this.originalQueryScope);
     Map<QueryScope, Set<CompiledMeasure>> result = new HashMap<>(Map.of(this.originalQueryScope, Set.of(cmrp.measure())));
     result.put(readScope, Set.of(cmrp.measure()));
     return result;
   }
 
+  @Override
+  public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledGrandTotalComparisonMeasure cgt) {
+    QueryScope readScope = MeasureUtils.getReadScopeComparisonGrandTotalMeasure(this.originalQueryScope);
+    Map<QueryScope, Set<CompiledMeasure>> result = new HashMap<>(Map.of(this.originalQueryScope, Set.of(cgt.measure())));
+    result.put(readScope, Set.of(cgt.measure()));
+    return result;
+  }
+
   @Override
   public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledDoubleConstantMeasure measure) {
     return empty();
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 0c41c32..eb1970e 100644
--- a/core/src/main/java/io/squashql/query/compiled/PrimitiveMeasureVisitor.java
+++ b/core/src/main/java/io/squashql/query/compiled/PrimitiveMeasureVisitor.java
@@ -28,7 +28,12 @@ public class PrimitiveMeasureVisitor implements MeasureVisitor<Boolean> {
   }
 
   @Override
-  public Boolean visit(CompiledComparisonMeasure measure) {
+  public Boolean visit(CompiledComparisonMeasureReferencePosition measure) {
+    return false;
+  }
+
+  @Override
+  public Boolean visit(CompiledGrandTotalComparisonMeasure compiledGrandTotalComparisonMeasure) {
     return false;
   }
 
diff --git a/documentation/QUERY.md b/documentation/QUERY.md
index 12ec97e..1364c32 100644
--- a/documentation/QUERY.md
+++ b/documentation/QUERY.md
@@ -951,6 +951,13 @@ Result
 +-------------+-------------+-------------+------------+---------------------+---------------------+
 ```
 
+`comparisonMeasureWithGrandTotal` method is used to create a special measure built to compare values of an underlying
+measure with the grand total values of the same measure.
+
+```typescript
+const gtRatio = comparisonMeasureWithGrandTotal("gt ratio", ComparisonMethod.DIVIDE, pop)
+```
+
 ##### Group comparison - ColumnSet
 
 This type of comparison is mainly used for what-if comparison but not limited to it. It involves the creation of a new
diff --git a/js/typescript-library/src/generate-json-queryDto.ts b/js/typescript-library/src/generate-json-queryDto.ts
index 31d03e4..f6f8b8a 100644
--- a/js/typescript-library/src/generate-json-queryDto.ts
+++ b/js/typescript-library/src/generate-json-queryDto.ts
@@ -2,7 +2,10 @@ import {JoinType, Query, Table} from "./query"
 import {
   AggregatedMeasure,
   avgIf,
-  BinaryOperationMeasure, BinaryOperator, comparisonMeasureWithGrandTotalAlongAncestors,
+  BinaryOperationMeasure,
+  BinaryOperator,
+  comparisonMeasureWithGrandTotal,
+  comparisonMeasureWithGrandTotalAlongAncestors,
   comparisonMeasureWithinSameGroup,
   comparisonMeasureWithParent,
   comparisonMeasureWithPeriod,
@@ -75,6 +78,7 @@ export function generateFromQueryDto() {
   ]), new Month(tableField("Month"), tableField("Year"))))
   q.withMeasure(comparisonMeasureWithParent("parent", ComparisonMethod.DIVIDE, price, [tableField("Year"), tableField("Month")]))
   q.withMeasure(comparisonMeasureWithGrandTotalAlongAncestors("grandTotalAlongAncestors", ComparisonMethod.DIVIDE, price, [tableField("Year"), tableField("Month")]))
+  q.withMeasure(comparisonMeasureWithGrandTotal("grandTotal", ComparisonMethod.DIVIDE, price))
 
   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 2fac822..b0e0445 100644
--- a/js/typescript-library/src/index.ts
+++ b/js/typescript-library/src/index.ts
@@ -16,6 +16,7 @@ export {
   plus, minus, multiply, divide,
   integer, decimal,
   comparisonMeasureWithPeriod, comparisonMeasureWithinSameGroup, comparisonMeasureWithParent, comparisonMeasureWithGrandTotalAlongAncestors,
+  comparisonMeasureWithGrandTotal,
   totalCount,
   ComparisonMethod,
 } from './measure'
diff --git a/js/typescript-library/src/measure.ts b/js/typescript-library/src/measure.ts
index 4f76631..59dbb34 100644
--- a/js/typescript-library/src/measure.ts
+++ b/js/typescript-library/src/measure.ts
@@ -14,19 +14,13 @@ export type BasicMeasure = Measure
 
 export class AggregatedMeasure implements BasicMeasure {
   readonly class: string = PACKAGE + "AggregatedMeasure"
-  readonly field: Field
-  readonly aggregationFunction: string
-  readonly alias: string
   readonly expression?: string
-  readonly distinct?: boolean
-  readonly criteria?: Criteria
 
-  constructor(alias: string, field: Field, aggregationFunction: string, distinct?: boolean, criterion?: Criteria) {
-    this.alias = alias
-    this.field = field
-    this.aggregationFunction = aggregationFunction
-    this.distinct = distinct
-    this.criteria = criterion
+  constructor(readonly alias: string,
+              readonly field: Field,
+              readonly aggregationFunction: string,
+              readonly distinct?: boolean,
+              readonly criteria?: Criteria) {
   }
 
   toJSON() {
@@ -44,10 +38,9 @@ export class AggregatedMeasure implements BasicMeasure {
 
 export class ExpressionMeasure implements BasicMeasure {
   readonly class: string = PACKAGE + "ExpressionMeasure"
-  readonly alias: string
 
-  constructor(alias: string, private sqlExpression: string) {
-    this.alias = alias
+  constructor(readonly alias: string,
+              private sqlExpression: string) {
   }
 
   toJSON() {
@@ -63,17 +56,13 @@ export const totalCount = new ExpressionMeasure("_total_count_", "COUNT(*) OVER
 
 export class BinaryOperationMeasure implements Measure {
   readonly class: string = PACKAGE + "BinaryOperationMeasure"
-  readonly alias: string
   readonly expression?: string
-  readonly operator: BinaryOperator
-  readonly leftOperand: Measure
-  readonly rightOperand: Measure
 
-  constructor(alias: string, operator: BinaryOperator, leftOperand: Measure, rightOperand: Measure) {
+  constructor(readonly alias: string,
+              readonly operator: BinaryOperator,
+              readonly leftOperand: Measure,
+              readonly rightOperand: Measure) {
     this.alias = alias
-    this.operator = operator
-    this.leftOperand = leftOperand
-    this.rightOperand = rightOperand
   }
 
   toJSON() {
@@ -96,18 +85,16 @@ export enum BinaryOperator {
 
 export class ComparisonMeasureReferencePosition implements Measure {
   readonly class: string = PACKAGE + "ComparisonMeasureReferencePosition"
-  readonly alias: string
   readonly expression?: string
 
-  constructor(alias: string,
-              private comparisonMethod: ComparisonMethod,
-              private measure: Measure,
-              private referencePosition: Map<Field, string>,
-              private columnSetKey?: ColumnSetKey,
-              private period?: Period,
-              private ancestors?: Array<Field>,
-              private grandTotalAlongAncestors?: boolean) {
-    this.alias = alias
+  constructor(readonly alias: string,
+              readonly comparisonMethod: ComparisonMethod,
+              readonly measure: Measure,
+              readonly referencePosition: Map<Field, string>,
+              readonly columnSetKey?: ColumnSetKey,
+              readonly period?: Period,
+              readonly ancestors?: Array<Field>,
+              readonly grandTotalAlongAncestors?: boolean) {
   }
 
   toJSON() {
@@ -125,6 +112,25 @@ export class ComparisonMeasureReferencePosition implements Measure {
   }
 }
 
+export class ComparisonMeasureGrandTotal implements Measure {
+  readonly class: string = PACKAGE + "ComparisonMeasureGrandTotal"
+  readonly expression?: string
+
+  constructor(readonly alias: string,
+              readonly comparisonMethod: ComparisonMethod,
+              readonly measure: Measure) {
+  }
+
+  toJSON() {
+    return {
+      "@class": this.class,
+      "alias": this.alias,
+      "comparisonMethod": this.comparisonMethod,
+      "measure": this.measure,
+    }
+  }
+}
+
 export enum ComparisonMethod {
   ABSOLUTE_DIFFERENCE = "ABSOLUTE_DIFFERENCE",
   RELATIVE_DIFFERENCE = "RELATIVE_DIFFERENCE",
@@ -135,7 +141,7 @@ export class LongConstantMeasure implements Measure {
   readonly class: string = PACKAGE + "LongConstantMeasure"
   readonly alias: string
 
-  constructor(private value: number) {
+  constructor(readonly value: number) {
   }
 
   toJSON() {
@@ -150,7 +156,7 @@ export class DoubleConstantMeasure implements Measure {
   readonly class: string = PACKAGE + "DoubleConstantMeasure"
   readonly alias: string
 
-  constructor(private value: number) {
+  constructor(readonly value: number) {
   }
 
   toJSON() {
@@ -274,3 +280,9 @@ export function comparisonMeasureWithGrandTotalAlongAncestors(alias: string,
                                                               ancestors: Array<Field>): Measure {
   return new ComparisonMeasureReferencePosition(alias, comparisonMethod, measure, undefined, undefined, undefined, ancestors, true)
 }
+
+export function comparisonMeasureWithGrandTotal(alias: string,
+                                                comparisonMethod: ComparisonMethod,
+                                                measure: Measure): Measure {
+  return new ComparisonMeasureGrandTotal(alias, comparisonMethod, measure)
+}


Test patch:
diff --git a/bigquery/src/test/java/io/squashql/query/TestBigQueryGrandTotalComparison.java b/bigquery/src/test/java/io/squashql/query/TestBigQueryGrandTotalComparison.java
new file mode 100644
index 0000000..48a2bb4
--- /dev/null
+++ b/bigquery/src/test/java/io/squashql/query/TestBigQueryGrandTotalComparison.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 TestBigQueryGrandTotalComparison extends ATestGrandTotalComparison {
+
+  @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/TestClickHouseGrandTotalComparison.java b/clickhouse/src/test/java/io/squashql/query/TestClickHouseGrandTotalComparison.java
new file mode 100644
index 0000000..53cb60c
--- /dev/null
+++ b/clickhouse/src/test/java/io/squashql/query/TestClickHouseGrandTotalComparison.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 TestClickHouseGrandTotalComparison extends ATestGrandTotalComparison {
+
+  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/query/ATestGrandTotalComparison.java b/core/src/test/java/io/squashql/query/ATestGrandTotalComparison.java
new file mode 100644
index 0000000..d4eb463
--- /dev/null
+++ b/core/src/test/java/io/squashql/query/ATestGrandTotalComparison.java
@@ -0,0 +1,139 @@
+package io.squashql.query;
+
+import io.squashql.TestClass;
+import io.squashql.query.builder.Query;
+import io.squashql.query.dto.QueryDto;
+import io.squashql.table.Table;
+import io.squashql.type.TableTypedField;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static io.squashql.query.TableField.tableField;
+import static io.squashql.query.TableField.tableFields;
+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 ATestGrandTotalComparison extends ABaseTestQuery {
+
+  private final String storeName = "store" + getClass().getSimpleName().toLowerCase();
+  private final List<Field> fields = tableFields(List.of("continent", "country", "city")); // ancestors
+  private final Measure amount = Functions.sum("amount", "amount");
+  private final ComparisonMeasureGrandTotal percentOfGT = new ComparisonMeasureGrandTotal("percentOfParent", ComparisonMethod.DIVIDE, this.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", 1d},
+            new Object[]{"paris", "france", "eu", "home", 2d},
+            new Object[]{"paris", "france", "eu", "hobbies", 1d},
+            new Object[]{"lyon", "france", "eu", "car", 0.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", 2d},
+            new Object[]{"london", "uk", "eu", "hobbies", 5d}
+    ));
+  }
+
+  @Test
+  void testSimple() {
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(this.fields, List.of(this.amount, this.percentOfGT))
+            .build();
+
+    Table result = this.executor.executeQuery(query);
+    double gt = 16.1d; // amount on GrandTotal
+    Assertions.assertThat(result).containsExactly(
+            Arrays.asList("eu", "france", "lyon", 3.1d, 3.1d / gt),
+            Arrays.asList("eu", "france", "paris", 4d, 4d / gt),
+            Arrays.asList("eu", "uk", "london", 9d, 9d / gt));
+
+    query = Query
+            .from(this.storeName)
+            .select(this.fields, List.of(this.amount, this.percentOfGT))
+            .rollup(this.fields)
+            .build();
+    result = this.executor.executeQuery(query);
+    Assertions.assertThat(result).containsExactly(
+            Arrays.asList(GRAND_TOTAL, GRAND_TOTAL, GRAND_TOTAL, 16.1d, 1d),
+            Arrays.asList("eu", TOTAL, TOTAL, 16.1d, 1d),
+            Arrays.asList("eu", "france", TOTAL, 7.1d, 7.1d / gt),
+            Arrays.asList("eu", "france", "lyon", 3.1d, 3.1d / gt),
+            Arrays.asList("eu", "france", "paris", 4d, 4d / gt),
+            Arrays.asList("eu", "uk", TOTAL, 9d, 9d / gt),
+            Arrays.asList("eu", "uk", "london", 9d, 9d / gt));
+  }
+
+  @Test
+  void testCrossjoinWithOtherColumn() {
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(tableFields(List.of("spending_category", "continent", "country", "city")), List.of(this.amount, this.percentOfGT))
+            .build();
+
+    Table result = this.executor.executeQuery(query);
+    double gt = 16.1d;
+    Assertions.assertThat(result).containsExactly(
+            Arrays.asList("car", "eu", "france", "lyon", 0.1d, 0.1d / gt),
+            Arrays.asList("car", "eu", "france", "paris", 1d, 1d / gt),
+            Arrays.asList("car", "eu", "uk", "london", 2d, 2d / gt),
+            Arrays.asList("hobbies", "eu", "france", "lyon", 1d, 1d / gt),
+            Arrays.asList("hobbies", "eu", "france", "paris", 1d, 1d / gt),
+            Arrays.asList("hobbies", "eu", "uk", "london", 5d, 5d / gt),
+            Arrays.asList("home", "eu", "france", "lyon", 2d, 2d / gt),
+            Arrays.asList("home", "eu", "france", "paris", 2d, 2d / gt),
+            Arrays.asList("home", "eu", "uk", "london", 2d, 2d / gt));
+  }
+
+  @Test
+  void testCrossjoinWithOtherColumnAnFilters() {
+    // Filter on other category
+    QueryDto query = Query
+            .from(this.storeName)
+            .where(Functions.criterion(tableField("spending_category"), Functions.eq("car")))
+            .select(tableFields(List.of("spending_category", "continent", "country", "city")), List.of(this.amount, this.percentOfGT))
+            .build();
+
+    Table result = this.executor.executeQuery(query);
+    double gt = 3.1d;
+    Assertions.assertThat(result).containsExactly(
+            Arrays.asList("car", "eu", "france", "lyon", 0.1d, 0.1d / gt),
+            Arrays.asList("car", "eu", "france", "paris", 1d, 1d / gt),
+            Arrays.asList("car", "eu", "uk", "london", 2d, 2d / gt));
+
+    query = Query
+            .from(this.storeName)
+            .where(Functions.criterion(tableField("country"), Functions.eq("france")))
+            .select(tableFields(List.of("spending_category", "continent", "country", "city")), List.of(this.amount, this.percentOfGT))
+            .build();
+
+    result = this.executor.executeQuery(query);
+    gt = 7.1d;
+    Assertions.assertThat(result).containsExactly(
+            Arrays.asList("car", "eu", "france", "lyon", 0.1d, 0.1d / gt),
+            Arrays.asList("car", "eu", "france", "paris", 1d, 1d / gt),
+            Arrays.asList("hobbies", "eu", "france", "lyon", 1d, 1d / gt),
+            Arrays.asList("hobbies", "eu", "france", "paris", 1d, 1d / gt),
+            Arrays.asList("home", "eu", "france", "lyon", 2d, 2d / gt),
+            Arrays.asList("home", "eu", "france", "paris", 2d, 2d / gt));
+  }
+}
diff --git a/duckdb/src/test/java/io/squashql/query/TestDuckDBGrandTotalComparison.java b/duckdb/src/test/java/io/squashql/query/TestDuckDBGrandTotalComparison.java
new file mode 100644
index 0000000..5e5a50e
--- /dev/null
+++ b/duckdb/src/test/java/io/squashql/query/TestDuckDBGrandTotalComparison.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 TestDuckDBGrandTotalComparison extends ATestGrandTotalComparison {
+
+  @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 c8e1431..35efd55 100644
--- a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
+++ b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
@@ -77,6 +77,9 @@ public class TestJavascriptLibrary {
             price,
             List.of(tableField("Year"), tableField("Month")),
             true));
+    q.withMeasure(new ComparisonMeasureGrandTotal("grandTotal",
+            ComparisonMethod.DIVIDE,
+            price));
 
     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/TestSnowflakeGrandTotalComparison.java b/snowflake/src/test/java/io/squashql/query/TestSnowflakeGrandTotalComparison.java
new file mode 100644
index 0000000..4e7e898
--- /dev/null
+++ b/snowflake/src/test/java/io/squashql/query/TestSnowflakeGrandTotalComparison.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 TestSnowflakeGrandTotalComparison extends ATestGrandTotalComparison {
+
+  @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/TestSparkGrandTotalComparison.java b/spark/src/test/java/io/squashql/query/TestSparkGrandTotalComparison.java
new file mode 100644
index 0000000..8e9fe84
--- /dev/null
+++ b/spark/src/test/java/io/squashql/query/TestSparkGrandTotalComparison.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 TestSparkGrandTotalComparison extends ATestGrandTotalComparison {
+
+  @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));
+  }
+}