Repository: squashql/squashql
Pull request #258 (2024-02-06 06:28:15)

Change return type of pivot query result to return ready cells instead of table


Patch:
diff --git a/core/src/main/java/io/squashql/query/QueryExecutor.java b/core/src/main/java/io/squashql/query/QueryExecutor.java
index e841452..8257b5c 100644
--- a/core/src/main/java/io/squashql/query/QueryExecutor.java
+++ b/core/src/main/java/io/squashql/query/QueryExecutor.java
@@ -70,13 +70,16 @@ public class QueryExecutor {
     Table result = executeQuery(preparedQuery, cacheStatsDtoBuilder, user, false, limitNotifier);
     if (replaceTotalCellsAndOrderRows) {
       result = TableUtils.replaceTotalCellValues((ColumnarTable) result,
-              pivotTableQueryDto.rows.stream().map(Field::name).toList(),
-              pivotTableQueryDto.columns.stream().map(Field::name).toList());
+              pivotTableQueryDto.rows.stream().map(SqlUtils::squashqlExpression).toList(),
+              pivotTableQueryDto.columns.stream().map(SqlUtils::squashqlExpression).toList());
       result = TableUtils.orderRows((ColumnarTable) result, Queries.getComparators(preparedQuery), preparedQuery.columnSets.values());
     }
 
     List<String> values = pivotTableQueryDto.query.measures.stream().map(Measure::alias).toList();
-    return new PivotTable(result, pivotTableQueryDto.rows.stream().map(Field::name).toList(), pivotTableQueryDto.columns.stream().map(Field::name).toList(), values);
+    return new PivotTable(result,
+            pivotTableQueryDto.rows.stream().map(SqlUtils::squashqlExpression).toList(),
+            pivotTableQueryDto.columns.stream().map(SqlUtils::squashqlExpression).toList(),
+            values);
   }
 
   private static QueryDto prepareQuery(QueryDto query, PivotTableContext context) {
@@ -87,13 +90,13 @@ public class QueryExecutor {
     axes.removeAll(select);
 
     if (!axes.isEmpty()) {
-      throw new IllegalArgumentException(axes.stream().map(Field::name).toList() + " on rows or columns by not in select. Please add those fields in select");
+      throw new IllegalArgumentException(axes.stream().map(SqlUtils::squashqlExpression).toList() + " on rows or columns by not in select. Please add those fields in select");
     }
     axes = new HashSet<>(context.rows);
     axes.addAll(context.columns);
     select.removeAll(axes);
     if (!select.isEmpty()) {
-      throw new IllegalArgumentException(select.stream().map(Field::name).toList() + " in select but not on rows or columns. Please add those fields on one axis");
+      throw new IllegalArgumentException(select.stream().map(SqlUtils::squashqlExpression).toList() + " in select but not on rows or columns. Please add those fields on one axis");
     }
 
     List<Field> rows = context.cleansedRows;
@@ -367,8 +370,8 @@ public class QueryExecutor {
 
   }
 
-  public PivotTable executePivotQueryMerge(QueryMergeDto queryMerge, List<Field> rows, List<Field> columns, SquashQLUser user) {
-    return QueryMergeExecutor.executePivotQueryMerge(this, queryMerge, rows, columns, user);
+  public PivotTable executePivotQueryMerge(PivotTableQueryMergeDto pivotTableQueryMergeDto, SquashQLUser user) {
+    return QueryMergeExecutor.executePivotQueryMerge(this, pivotTableQueryMergeDto, user);
   }
 
   public Table executeQueryMerge(QueryMergeDto queryMerge, SquashQLUser user) {
diff --git a/core/src/main/java/io/squashql/query/QueryMergeExecutor.java b/core/src/main/java/io/squashql/query/QueryMergeExecutor.java
index 5a4cbc2..739f959 100644
--- a/core/src/main/java/io/squashql/query/QueryMergeExecutor.java
+++ b/core/src/main/java/io/squashql/query/QueryMergeExecutor.java
@@ -1,9 +1,7 @@
 package io.squashql.query;
 
-import io.squashql.query.dto.CacheStatsDto;
-import io.squashql.query.dto.PivotTableQueryDto;
-import io.squashql.query.dto.QueryDto;
-import io.squashql.query.dto.QueryMergeDto;
+import io.squashql.query.database.SqlUtils;
+import io.squashql.query.dto.*;
 import io.squashql.query.exception.LimitExceedException;
 import io.squashql.table.*;
 import io.squashql.util.Queries;
@@ -13,6 +11,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class QueryMergeExecutor {
 
@@ -28,13 +27,16 @@ public class QueryMergeExecutor {
     return execute(queryMerge, t -> (ColumnarTable) TableUtils.replaceTotalCellValues((ColumnarTable) t, true), executor);
   }
 
-  public static PivotTable executePivotQueryMerge(QueryExecutor queryExecutor, QueryMergeDto queryMerge, List<Field> rows, List<Field> columns, SquashQLUser user) {
+  public static PivotTable executePivotQueryMerge(QueryExecutor queryExecutor, PivotTableQueryMergeDto pivotTableQueryMergeDto, SquashQLUser user) {
+    List<Field> rows = pivotTableQueryMergeDto.rows;
+    List<Field> columns = pivotTableQueryMergeDto.columns;
     Function<QueryDto, Table> executor = query -> {
       Set<Field> columnsFromColumnSets = query.columnSets.values().stream().flatMap(cs -> cs.getNewColumns().stream()).collect(Collectors.toSet());
+      List<Field> localRows = getLocalFields(rows, query, columnsFromColumnSets);
+      List<Field> localColumns = getLocalFields(columns, query, columnsFromColumnSets);
+
       return queryExecutor.executePivotQuery(
-              new PivotTableQueryDto(query,
-                      rows.stream().filter(r -> query.columns.contains(r) || columnsFromColumnSets.contains(r)).toList(),
-                      columns.stream().filter(r -> query.columns.contains(r) || columnsFromColumnSets.contains(r)).toList()),
+              new PivotTableQueryDto(query, localRows, localColumns, false),
               CacheStatsDto.builder(),
               user,
               false,
@@ -45,11 +47,25 @@ public class QueryMergeExecutor {
     };
 
     Function<Table, ColumnarTable> replaceTotalCellValuesFunction = t -> (ColumnarTable) TableUtils.replaceTotalCellValues((ColumnarTable) t,
-            rows.stream().map(Field::name).toList(),
-            columns.stream().map(Field::name).toList());
-    ColumnarTable table = execute(queryMerge, replaceTotalCellValuesFunction, executor);
+            rows.stream().map(SqlUtils::squashqlExpression).toList(),
+            columns.stream().map(SqlUtils::squashqlExpression).toList());
+    ColumnarTable table = execute(pivotTableQueryMergeDto.query, replaceTotalCellValuesFunction, executor);
     List<String> values = table.headers().stream().filter(Header::isMeasure).map(Header::name).toList();
-    return new PivotTable(table, rows.stream().map(Field::name).toList(), columns.stream().map(Field::name).toList(), values);
+    return new PivotTable(table,
+            rows.stream().map(SqlUtils::squashqlExpression).toList(),
+            columns.stream().map(SqlUtils::squashqlExpression).toList(),
+            values);
+  }
+
+  private static List<Field> getLocalFields(List<Field> elements, QueryDto query, Set<Field> columnsFromColumnSets) {
+    List<Field> localElements = new ArrayList<>();
+    for (Field element : elements) {
+      Stream.concat(query.columns.stream(), columnsFromColumnSets.stream())
+              .filter(f -> SqlUtils.squashqlExpression(f).equals(SqlUtils.squashqlExpression(element)))
+              .findFirst()
+              .ifPresent(localElements::add); // add the column from the select for the check done later.
+    }
+    return localElements;
   }
 
   private static ColumnarTable execute(QueryMergeDto queryMerge,
diff --git a/core/src/main/java/io/squashql/query/database/SqlUtils.java b/core/src/main/java/io/squashql/query/database/SqlUtils.java
index 99d0b44..a2e1063 100644
--- a/core/src/main/java/io/squashql/query/database/SqlUtils.java
+++ b/core/src/main/java/io/squashql/query/database/SqlUtils.java
@@ -1,5 +1,8 @@
 package io.squashql.query.database;
 
+import io.squashql.query.Field;
+import io.squashql.query.FunctionField;
+import io.squashql.query.TableField;
 import io.squashql.type.FunctionTypedField;
 import io.squashql.type.TableTypedField;
 import io.squashql.type.TypedField;
@@ -51,6 +54,20 @@ public class SqlUtils {
     }
   }
 
+  public static String squashqlExpression(Field f) {
+    if (f.alias() != null) {
+      return f.alias();
+    }
+
+    if (f instanceof TableField tf) {
+      return getFieldFullName(tf.tableName, tf.fieldName);
+    } else if (f instanceof FunctionField ftf) {
+      return singleOperandFunctionName(ftf.function, squashqlExpression(f));
+    } else {
+      throw new IllegalArgumentException(f.getClass().getName());
+    }
+  }
+
   public static String singleOperandFunctionName(String function, String operand) {
     return function + "(" + operand + ")";
   }
diff --git a/core/src/main/java/io/squashql/query/dto/PivotTableQueryDto.java b/core/src/main/java/io/squashql/query/dto/PivotTableQueryDto.java
index aadb9f9..fc60caa 100644
--- a/core/src/main/java/io/squashql/query/dto/PivotTableQueryDto.java
+++ b/core/src/main/java/io/squashql/query/dto/PivotTableQueryDto.java
@@ -17,4 +17,5 @@ public class PivotTableQueryDto {
   public QueryDto query;
   public List<Field> rows;
   public List<Field> columns;
+  public Boolean minify;
 }
diff --git a/core/src/main/java/io/squashql/query/dto/PivotTableQueryMergeDto.java b/core/src/main/java/io/squashql/query/dto/PivotTableQueryMergeDto.java
index 2e97e61..9596c8c 100644
--- a/core/src/main/java/io/squashql/query/dto/PivotTableQueryMergeDto.java
+++ b/core/src/main/java/io/squashql/query/dto/PivotTableQueryMergeDto.java
@@ -17,4 +17,5 @@ public class PivotTableQueryMergeDto {
   public QueryMergeDto query;
   public List<Field> rows;
   public List<Field> columns;
+  public Boolean minify;
 }
diff --git a/core/src/main/java/io/squashql/query/dto/PivotTableQueryResultDto.java b/core/src/main/java/io/squashql/query/dto/PivotTableQueryResultDto.java
index fc402e8..310ec14 100644
--- a/core/src/main/java/io/squashql/query/dto/PivotTableQueryResultDto.java
+++ b/core/src/main/java/io/squashql/query/dto/PivotTableQueryResultDto.java
@@ -3,6 +3,7 @@ package io.squashql.query.dto;
 import lombok.*;
 
 import java.util.List;
+import java.util.Map;
 
 @Builder
 @ToString
@@ -12,7 +13,7 @@ import java.util.List;
 @Getter
 public class PivotTableQueryResultDto {
 
-  public QueryResultDto queryResult;
+  public List<Map<String, Object>> cells;
   public List<String> rows;
   public List<String> columns;
   public List<String> values;
diff --git a/core/src/main/java/io/squashql/table/PivotTableUtils.java b/core/src/main/java/io/squashql/table/PivotTableUtils.java
index d8fd69a..7553388 100644
--- a/core/src/main/java/io/squashql/table/PivotTableUtils.java
+++ b/core/src/main/java/io/squashql/table/PivotTableUtils.java
@@ -1,6 +1,9 @@
 package io.squashql.table;
 
+import io.squashql.query.Header;
 import io.squashql.util.ListUtils;
+import io.squashql.util.NullAndTotalComparator;
+import org.eclipse.collections.impl.list.mutable.primitive.IntArrayList;
 
 import java.util.*;
 import java.util.function.Supplier;
@@ -62,19 +65,19 @@ public final class PivotTableUtils {
     });
 
     List<List<Object>> finalRows = new ArrayList<>();
-    Supplier<List<Object>> listSpwaner = () -> {
+    Supplier<List<Object>> listSpawner = () -> {
       finalRows.add(new ArrayList<>(rows.size() + size));
       return finalRows.get(finalRows.size() - 1);
     };
     for (int i = 0; i < columns.size(); i++) {
-      List<Object> r = listSpwaner.get();
+      List<Object> r = listSpawner.get();
       for (int j = 0; j < rows.size(); j++) {
         r.add(columns.get(i)); // recopy name of the column
       }
       r.addAll(headerColumns.get(i));
     }
 
-    List<Object> r = listSpwaner.get();
+    List<Object> r = listSpawner.get();
     r.addAll(rows);
     for (int i = 0; i < columnHeaderValues.size(); i++) {
       r.addAll(values); // Recopy measure names
@@ -82,7 +85,7 @@ public final class PivotTableUtils {
 
     int[] index = new int[1];
     rowHeaderValues.forEach(a -> {
-      List<Object> rr = listSpwaner.get();
+      List<Object> rr = listSpawner.get();
       rr.addAll(Arrays.asList(a.a));
       rr.addAll(cells.get(index[0]++));
     });
@@ -125,6 +128,103 @@ public final class PivotTableUtils {
     return mapping;
   }
 
+  private static Map<String, BitSet> findNullValuesOnEntireColumn(PivotTable pivotTable) {
+    int[] rowIndices = getHeaderIndices(pivotTable.table, pivotTable.columns);
+    int[] measureIndices = getHeaderIndices(pivotTable.table, pivotTable.values);
+    int[] line = new int[1];
+    Map<ObjectArrayKey, Set<Object>[]> distinctValuesByKey = new HashMap<>();
+    Map<ObjectArrayKey, IntArrayList> lineByKey = new HashMap<>();
+    pivotTable.table.forEach(row -> {
+      Object[] coord = new Object[rowIndices.length];
+      for (int i = 0; i < rowIndices.length; i++) {
+        coord[i] = row.get(rowIndices[i]);
+      }
+
+      ObjectArrayKey key = new ObjectArrayKey(coord);
+      Set<Object>[] distinctValues = distinctValuesByKey.computeIfAbsent(key, k -> {
+        Set[] sets = new Set[measureIndices.length];
+        for (int i = 0; i < measureIndices.length; i++) {
+          sets[i] = new HashSet();
+        }
+        return sets;
+      });
+
+      for (int i = 0; i < measureIndices.length; i++) {
+        distinctValues[i].add(row.get(measureIndices[i]));
+      }
+      lineByKey.computeIfAbsent(key, k -> new IntArrayList()).add(line[0]);
+      line[0]++;
+    });
+
+    BitSet[] bitSets = new BitSet[measureIndices.length];
+    for (int i = 0; i < measureIndices.length; i++) {
+      bitSets[i] = new BitSet(line[0]);
+    }
+    for (Map.Entry<ObjectArrayKey, Set<Object>[]> entry : distinctValuesByKey.entrySet()) {
+      Set<Object>[] distinctValues = entry.getValue();
+      for (int i = 0; i < measureIndices.length; i++) {
+        if (distinctValues[i].size() == 1 && distinctValues[i].iterator().next() == null) {
+          // only nulls values.
+          int finalI = i;
+          lineByKey.get(entry.getKey()).forEach(index -> bitSets[finalI].set(index));
+        }
+      }
+    }
+
+    Map<String, BitSet> result = new HashMap<>();
+    for (int i = 0; i < pivotTable.values.size(); i++) {
+      result.put(pivotTable.values.get(i), bitSets[i]);
+    }
+    return result;
+  }
+
+  /**
+   * Generates cells of the pivot table. Entire column of null values are removed if minify set to true or null (default).
+   * For instance, if the pivot table looks like this:
+   * <pre>
+   * +-------------+-------------+-------------+-------------+--------+------------+---------------------+---------------------+
+   * |    category |    category | Grand Total | Grand Total |  extra |      extra | minimum expenditure | minimum expenditure |
+   * |   continent |     country |      amount |  population | amount | population |              amount |          population |
+   * +-------------+-------------+-------------+-------------+--------+------------+---------------------+---------------------+
+   * | Grand Total | Grand Total |        56.0 |       465.0 |   17.0 |       null |                39.0 |                null |
+   * |          am |       Total |        39.0 |       330.0 |   10.0 |       null |                29.0 |                null |
+   * |          am |         usa |        39.0 |       330.0 |   10.0 |       null |                29.0 |                null |
+   * |          eu |       Total |        17.0 |       135.0 |    7.0 |       null |                10.0 |                null |
+   * |          eu |      france |         8.0 |        70.0 |    2.0 |       null |                 6.0 |                null |
+   * |          eu |          uk |         9.0 |        65.0 |    5.0 |       null |                 4.0 |                null |
+   * +-------------+-------------+-------------+-------------+--------+------------+---------------------+---------------------+
+   * </pre>
+   * The two columns for extra/population and minimum expenditure/population are removed.
+   */
+  public static List<Map<String, Object>> generateCells(PivotTable pivotTable, Boolean minify) {
+    Map<String, BitSet> empty = new HashMap<>();
+    for (String value : pivotTable.values) {
+      empty.put(value, null);
+    }
+
+    Map<String, BitSet> bitSetByValue = minify == null || minify
+            ? PivotTableUtils.findNullValuesOnEntireColumn(pivotTable)
+            : empty;
+
+    List<Map<String, Object>> cells = new ArrayList<>((int) pivotTable.table.count());
+    List<String> headerNames = pivotTable.table.headers().stream().map(Header::name).toList();
+    int[] line = new int[1];
+    pivotTable.table.forEach(row -> {
+      Map<String, Object> cell = new HashMap<>();
+      for (int i = 0; i < row.size(); i++) {
+        Object value = row.get(i);
+
+        BitSet bitSet = bitSetByValue.get(headerNames.get(i));
+        if ((bitSet == null && !NullAndTotalComparator.isTotal(value)) || (bitSet != null && !bitSet.get(line[0]))) {
+          cell.put(headerNames.get(i), value);
+        }
+      }
+      line[0]++;
+      cells.add(cell);
+    });
+    return cells;
+  }
+
   private record ObjectArrayKey(Object[] a) {
 
     @Override
diff --git a/js/typescript-library/build-from-query-pivot.json b/js/typescript-library/build-from-query-pivot.json
index d1dcdcf..c6da9bd 100644
--- a/js/typescript-library/build-from-query-pivot.json
+++ b/js/typescript-library/build-from-query-pivot.json
@@ -1,1 +1,1 @@
-{"query":{"table":{"name":"myTable","joins":[]},"virtualTableDtos":[],"columns":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"},{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f1","fieldName":"f1"},"aggregationFunction":"avg","alias":"sum"}],"orders":{},"limit":-1},"rows":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"}],"columns":[{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}]}
\ No newline at end of file
+{"query":{"table":{"name":"myTable","joins":[]},"virtualTableDtos":[],"columns":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"},{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f1","fieldName":"f1"},"aggregationFunction":"avg","alias":"sum"}],"orders":{},"limit":-1},"rows":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"}],"columns":[{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}],"minify":true}
\ No newline at end of file
diff --git a/js/typescript-library/package-lock.json b/js/typescript-library/package-lock.json
index dc0886b..1f76b62 100644
--- a/js/typescript-library/package-lock.json
+++ b/js/typescript-library/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@squashql/squashql-js",
-  "version": "1.7.3",
+  "version": "1.7.3-test",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@squashql/squashql-js",
-      "version": "1.7.3",
+      "version": "1.7.3-test",
       "license": "Apache-2.0",
       "dependencies": {
         "axios": "^1.6.7"
diff --git a/js/typescript-library/package.json b/js/typescript-library/package.json
index c539130..c031235 100644
--- a/js/typescript-library/package.json
+++ b/js/typescript-library/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@squashql/squashql-js",
-  "version": "1.7.3",
+  "version": "1.7.3-test",
   "description": "SquashQL query builder/helper",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
diff --git a/js/typescript-library/pom.xml b/js/typescript-library/pom.xml
index cf190ff..8f71226 100644
--- a/js/typescript-library/pom.xml
+++ b/js/typescript-library/pom.xml
@@ -15,8 +15,8 @@
   <artifactId>squashql-js</artifactId>
 
   <properties>
-    <node.version>v16.17.0</node.version>
-    <npm.version>8.18.0</npm.version>
+    <node.version>v21.5.0</node.version>
+    <npm.version>10.4.0</npm.version>
   </properties>
 
   <dependencies>
@@ -33,7 +33,7 @@
       <plugin>
         <groupId>com.github.eirslett</groupId>
         <artifactId>frontend-maven-plugin</artifactId>
-        <version>1.12.1</version>
+        <version>1.15.0</version>
         <executions>
           <execution>
             <!-- optional: you don't really need execution ids, but it looks nice in your build log. -->
diff --git a/js/typescript-library/src/generate-json-query-merge-pivot.ts b/js/typescript-library/src/generate-json-query-merge-pivot.ts
index 985bb04..741f0c3 100644
--- a/js/typescript-library/src/generate-json-query-merge-pivot.ts
+++ b/js/typescript-library/src/generate-json-query-merge-pivot.ts
@@ -8,16 +8,16 @@ import {JoinType, QueryMerge} from "./query"
 export function generateFromQueryMergePivot() {
   const fields = tableFields(["a", "b"])
   const query1 = from("myTable")
-      .select(fields, [], [sum("sum", new TableField("f1"))])
-      .build()
+          .select(fields, [], [sum("sum", new TableField("f1"))])
+          .build()
 
   const query2 = from("myTable")
-      .select(fields, [], [avg("sum", new TableField("f1"))])
-      .build()
+          .select(fields, [], [avg("sum", new TableField("f1"))])
+          .build()
 
   const q = new QueryMerge(query1).join(query2, JoinType.LEFT)
 
-  const pivotQuery = createPivotTableQueryMerge(q, {rows: [tableField("a")], columns: [tableField("b")]})
+  const pivotQuery = createPivotTableQueryMerge(q, {rows: [tableField("a")], columns: [tableField("b")], minify: false})
   const data = JSON.stringify(pivotQuery)
   fs.writeFileSync('build-from-query-merge-pivot.json', data)
 }
diff --git a/js/typescript-library/src/generate-json-query-pivot.ts b/js/typescript-library/src/generate-json-query-pivot.ts
index a45db15..71c5ee8 100644
--- a/js/typescript-library/src/generate-json-query-pivot.ts
+++ b/js/typescript-library/src/generate-json-query-pivot.ts
@@ -13,7 +13,7 @@ export function generateFromQueryPivot() {
                   [avg("sum", new TableField("f1"))])
           .build()
 
-  const pivotQuery = createPivotTableQuery(q, {rows: [tableField("a")], columns: [tableField("b")]})
+  const pivotQuery = createPivotTableQuery(q, {rows: [tableField("a")], columns: [tableField("b")], minify: true})
   const data = JSON.stringify(pivotQuery)
   fs.writeFileSync('build-from-query-pivot.json', data)
 }
diff --git a/js/typescript-library/src/pivotTableQuery.ts b/js/typescript-library/src/pivotTableQuery.ts
index be27588..42ea464 100644
--- a/js/typescript-library/src/pivotTableQuery.ts
+++ b/js/typescript-library/src/pivotTableQuery.ts
@@ -5,15 +5,18 @@ export interface PivotTableQuery {
   query: Query
   rows: Array<Field>
   columns: Array<Field>
+  minify?: boolean
 }
 
 export interface PivotTableQueryMerge {
   query: QueryMerge
   rows: Array<Field>
   columns: Array<Field>
+  minify?: boolean
 }
 
 export interface PivotConfig {
   rows: Array<Field>
   columns: Array<Field>
+  minify?: boolean
 }
diff --git a/js/typescript-library/src/querier.ts b/js/typescript-library/src/querier.ts
index 81ff2cf..357e55d 100644
--- a/js/typescript-library/src/querier.ts
+++ b/js/typescript-library/src/querier.ts
@@ -68,11 +68,11 @@ export class Querier {
 }
 
 export function createPivotTableQuery(query: Query, pivotConfig: PivotConfig): PivotTableQuery {
-  return {query, rows: pivotConfig.rows, columns: pivotConfig.columns}
+  return {query, rows: pivotConfig.rows, columns: pivotConfig.columns, minify: pivotConfig.minify}
 }
 
 export function createPivotTableQueryMerge(query: QueryMerge, pivotConfig: PivotConfig): PivotTableQueryMerge {
-  return {query, rows: pivotConfig.rows, columns: pivotConfig.columns}
+  return {query, rows: pivotConfig.rows, columns: pivotConfig.columns, minify: pivotConfig.minify}
 }
 
 export interface QueryResult {
@@ -82,7 +82,7 @@ export interface QueryResult {
 }
 
 export interface PivotTableQueryResult {
-  queryResult: QueryResult,
+  cells: Array<Map<string, any>>,
   rows: Array<string>
   columns: Array<string>
   values: Array<string>
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 d70b7c5..f63d18d 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
@@ -6,6 +6,7 @@ import io.squashql.query.database.QueryEngine;
 import io.squashql.query.dto.*;
 import io.squashql.store.Store;
 import io.squashql.table.PivotTable;
+import io.squashql.table.PivotTableUtils;
 import io.squashql.table.Table;
 import io.squashql.table.TableUtils;
 import org.springframework.context.annotation.Import;
@@ -15,10 +16,7 @@ import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -76,17 +74,8 @@ public class QueryController {
             this.squashQLUserSupplier == null ? null : this.squashQLUserSupplier.get(),
             true,
             null);
-    List<String> fields = pt.table.headers().stream().map(Header::name).collect(Collectors.toList());
-    SimpleTableDto simpleTable = SimpleTableDto.builder()
-            .rows(ImmutableList.copyOf(pt.table.iterator()))
-            .columns(fields)
-            .build();
-    QueryResultDto result = QueryResultDto.builder()
-            .table(simpleTable)
-            .metadata(TableUtils.buildTableMetadata(pt.table))
-            .debug(DebugInfoDto.builder().cache(csBuilder.build()).build())
-            .build();
-    return ResponseEntity.ok(new PivotTableQueryResultDto(result, pt.rows, pt.columns, pt.values));
+    List<Map<String, Object>> cells = PivotTableUtils.generateCells(pt, pivotTableQueryDto.minify);
+    return ResponseEntity.ok(new PivotTableQueryResultDto(cells, pt.rows, pt.columns, pt.values));
   }
 
   @PostMapping(MAPPING_QUERY_MERGE)
@@ -100,13 +89,11 @@ public class QueryController {
   @PostMapping(MAPPING_QUERY_MERGE_PIVOT)
   public ResponseEntity<PivotTableQueryResultDto> executeQueryMergePivot(@RequestBody PivotTableQueryMergeDto pivotTableQueryMergeDto) {
     PivotTable pt = this.queryExecutor.executePivotQueryMerge(
-            pivotTableQueryMergeDto.query,
-            pivotTableQueryMergeDto.rows,
-            pivotTableQueryMergeDto.columns,
+            pivotTableQueryMergeDto,
             this.squashQLUserSupplier == null ? null : this.squashQLUserSupplier.get()
     );
-    QueryResultDto result = createQueryResultDto(pt.table);
-    return ResponseEntity.ok(new PivotTableQueryResultDto(result, pt.rows, pt.columns, pt.values));
+    List<Map<String, Object>> cells = PivotTableUtils.generateCells(pt, pivotTableQueryMergeDto.minify);
+    return ResponseEntity.ok(new PivotTableQueryResultDto(cells, pt.rows, pt.columns, pt.values));
   }
 
   @PostMapping(MAPPING_QUERY_JOIN_EXPERIMENTAL)
@@ -151,9 +138,7 @@ public class QueryController {
   @PostMapping(MAPPING_QUERY_MERGE_PIVOT_STRINGIFY)
   public ResponseEntity<String> executeAndMergePivotStringify(@RequestBody PivotTableQueryMergeDto pivotTableQueryMergeDto) {
     PivotTable pt = this.queryExecutor.executePivotQueryMerge(
-            pivotTableQueryMergeDto.query,
-            pivotTableQueryMergeDto.rows,
-            pivotTableQueryMergeDto.columns,
+            pivotTableQueryMergeDto,
             this.squashQLUserSupplier == null ? null : this.squashQLUserSupplier.get()
     );
     return ResponseEntity.ok(pt.toString());


Test patch:
diff --git a/core/src/test/java/io/squashql/jackson/TestQueryS13n.java b/core/src/test/java/io/squashql/jackson/TestQueryS13n.java
index 70a5afb..1b02a85 100644
--- a/core/src/test/java/io/squashql/jackson/TestQueryS13n.java
+++ b/core/src/test/java/io/squashql/jackson/TestQueryS13n.java
@@ -198,7 +198,7 @@ public class TestQueryS13n {
     QueryDto query = Query.from("table")
             .select(List.of(f1, f2), List.of(), List.of())
             .build();
-    PivotTableQueryDto pivotTableQueryDto = new PivotTableQueryDto(query, List.of(f1), List.of(f2));
+    PivotTableQueryDto pivotTableQueryDto = new PivotTableQueryDto(query, List.of(f1), List.of(f2), false);
     String serialize = JacksonUtil.serialize(pivotTableQueryDto);
     PivotTableQueryDto deserialize = JacksonUtil.deserialize(serialize, PivotTableQueryDto.class);
     Assertions.assertThat(deserialize).isEqualTo(pivotTableQueryDto);
diff --git a/core/src/test/java/io/squashql/query/ATestPivotTable.java b/core/src/test/java/io/squashql/query/ATestPivotTable.java
index 830221f..4b8b7bd 100644
--- a/core/src/test/java/io/squashql/query/ATestPivotTable.java
+++ b/core/src/test/java/io/squashql/query/ATestPivotTable.java
@@ -25,12 +25,21 @@ import static io.squashql.query.TableField.tableField;
 import static io.squashql.query.TableField.tableFields;
 import static io.squashql.query.database.QueryEngine.GRAND_TOTAL;
 
-@TestClass(ignore = {TestClass.Type.SNOWFLAKE})
+@TestClass
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public abstract class ATestPivotTable extends ABaseTestQuery {
 
-  protected String storeSpending = "storespending" + getClass().getSimpleName().toLowerCase();
-  protected String storePopulation = "storepopulation" + getClass().getSimpleName().toLowerCase();
+  private final String storeSpending = "storespending" + ATestPivotTable.class.getSimpleName().toLowerCase();
+  private final String storePopulation = "storepopulation" + ATestPivotTable.class.getSimpleName().toLowerCase();
+  private final TableField city = new TableField(this.storeSpending, "city");
+  private final TableField country = new TableField(this.storeSpending, "country");
+  private final TableField continent = new TableField(this.storeSpending, "continent");
+  private final TableField spendingCategory = new TableField(this.storeSpending, "spending category");
+  private final TableField spendingSubcategory = new TableField(this.storeSpending, "spending subcategory");
+  private final TableField amount = new TableField(this.storeSpending, "amount");
+  private final TableField population = new TableField(this.storePopulation, "population");
+  private final TableField countryPop = new TableField(this.storePopulation, "country");
+  private final TableField continentPop = new TableField(this.storePopulation, "continent");
 
   @Override
   protected Map<String, List<TableTypedField>> getFieldsByStore() {
@@ -41,9 +50,11 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
     TableTypedField spendingSubcategory = new TableTypedField(this.storeSpending, "spending subcategory", String.class);
     TableTypedField amount = new TableTypedField(this.storeSpending, "amount", double.class);
     TableTypedField population = new TableTypedField(this.storePopulation, "population", double.class);
+    TableTypedField countryPop = new TableTypedField(this.storePopulation, "country", String.class);
+    TableTypedField continentPop = new TableTypedField(this.storePopulation, "continent", String.class);
     return Map.of(
             this.storeSpending, List.of(city, country, continent, spendingCategory, spendingSubcategory, amount),
-            this.storePopulation, List.of(country, continent, population));
+            this.storePopulation, List.of(countryPop, continentPop, population));
   }
 
   @Override
@@ -101,12 +112,12 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
 
     // NO COLUMNS
     {
-      PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(queryWithoutRollup, fields, List.of()));
+      PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(queryWithoutRollup, fields, List.of(), false));
       Assertions.assertThat(result.table).containsExactlyInAnyOrderElementsOf(resultRollup);
     }
     // NO ROWS
     {
-      PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(queryWithoutRollup, List.of(), fields));
+      PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(queryWithoutRollup, List.of(), fields, false));
       Assertions.assertThat(result.table).containsExactlyInAnyOrderElementsOf(resultRollup);
     }
   }
@@ -125,7 +136,7 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
             .build();
     List<Field> rows = tableFields(List.of("city"));
     List<Field> columns = tableFields(List.of("spending category"));
-    PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(query, rows, columns));
+    PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(query, rows, columns, false));
 
     Assertions.assertThat(result.table).containsExactly(
             List.of(GRAND_TOTAL, GRAND_TOTAL, 22d),
@@ -150,6 +161,45 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
     );
   }
 
+  /**
+   * Simple case.
+   */
+  @Test
+  void testOneColumnEachAxisFullName() {
+    Measure amount = Functions.sum("amount", this.amount);
+
+    QueryDto query = Query
+            .from(this.storeSpending)
+            .where(criterion(this.city, in("la", "london"))) // to reduce size of the output
+            .select(List.of(this.spendingCategory, this.city), List.of(amount))
+            .build();
+    List<Field> rows = List.of(this.city);
+    List<Field> columns = List.of(this.spendingCategory);
+    PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(query, rows, columns, false));
+
+    Assertions.assertThat(result.table).containsExactly(
+            List.of(GRAND_TOTAL, GRAND_TOTAL, 22d),
+            List.of(GRAND_TOTAL, "la", 13d),
+            List.of(GRAND_TOTAL, "london", 9d),
+
+            List.of("extra", GRAND_TOTAL, 9d),
+            List.of("extra", "la", 4d),
+            List.of("extra", "london", 5d),
+
+            List.of("minimum expenditure", GRAND_TOTAL, 13d),
+            List.of("minimum expenditure", "la", 9d),
+            List.of("minimum expenditure", "london", 4d)
+    );
+
+    Assertions.assertThat(result.pivotTableCells).containsExactly(
+            List.of(this.storeSpending + ".spending category", GRAND_TOTAL, "extra", "minimum expenditure"),
+            List.of(this.storeSpending + ".city", "amount", "amount", "amount"),
+            List.of(GRAND_TOTAL, 22d, 9d, 13d),
+            List.of("la", 13d, 4d, 9d),
+            List.of("london", 9d, 5d, 4d)
+    );
+  }
+
   @Test
   void testComplexPivotTableSingleMeasure(TestInfo testInfo) {
     Measure amount = Functions.sum("amount", "amount");
@@ -205,7 +255,7 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
             .select(List.of(), List.of(bucketCS), measures)
             .build();
     PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(query, Collections.singletonList(country),
-            Collections.singletonList(group)));
+            Collections.singletonList(group), false));
     Assertions.assertThat(result.table).containsExactly(
             List.of("european", "uk", 0d),
             List.of("european", "france", -1d),
@@ -282,7 +332,7 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
             .build();
     List<Field> rows = tableFields(List.of("continent", "country", "city"));
     List<Field> columns = tableFields(List.of("spending category", "spending subcategory"));
-    Assertions.assertThatThrownBy(() -> this.executor.executePivotQuery(new PivotTableQueryDto(query, rows, columns)))
+    Assertions.assertThatThrownBy(() -> this.executor.executePivotQuery(new PivotTableQueryDto(query, rows, columns, false)))
             .hasMessage("Rollup is not supported by this API");
   }
 
@@ -297,13 +347,13 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
     List<Field> rowsWithoutContinent = tableFields(List.of("country", "city"));
     List<Field> columns = tableFields(List.of("spending category", "spending subcategory"));
     // continent is missing despite the fact it is in the select
-    Assertions.assertThatThrownBy(() -> this.executor.executePivotQuery(new PivotTableQueryDto(query, rowsWithoutContinent, columns)))
+    Assertions.assertThatThrownBy(() -> this.executor.executePivotQuery(new PivotTableQueryDto(query, rowsWithoutContinent, columns, false)))
             .hasMessage("[continent] in select but not on rows or columns. Please add those fields on one axis");
 
     List<Field> rows = tableFields(List.of("continent", "country", "city"));
     List<Field> columnsWithoutContinent = tableFields(List.of("spending category"));
     // spending subcategory is missing despite the fact it is in the select
-    Assertions.assertThatThrownBy(() -> this.executor.executePivotQuery(new PivotTableQueryDto(query, rows, columnsWithoutContinent)))
+    Assertions.assertThatThrownBy(() -> this.executor.executePivotQuery(new PivotTableQueryDto(query, rows, columnsWithoutContinent, false)))
             .hasMessage("[spending subcategory] in select but not on rows or columns. Please add those fields on one axis");
   }
 
@@ -318,14 +368,14 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
     List<Field> rows = tableFields(List.of("unknown", "continent", "country", "city"));
     List<Field> columns = tableFields(List.of("spending category", "spending subcategory"));
     // continent is missing despite the fact it is in the select
-    Assertions.assertThatThrownBy(() -> this.executor.executePivotQuery(new PivotTableQueryDto(query, rows, columns)))
+    Assertions.assertThatThrownBy(() -> this.executor.executePivotQuery(new PivotTableQueryDto(query, rows, columns, false)))
             .hasMessage("[unknown] on rows or columns by not in select. Please add those fields in select");
   }
 
   @Test
   void testDrillingAcross(TestInfo testInfo) {
-    Measure amount = Functions.sum("amount", "amount");
-    Measure pop = Functions.sum("population", "population");
+    Measure amount = Functions.sum("amount", this.amount);
+    Measure pop = Functions.sum("population", this.population);
 
     List<Measure> measuresSpending = List.of(amount);
     QueryDto query1 = Query
@@ -334,12 +384,40 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
             .build();
 
     List<Measure> measuresPop = List.of(pop);
+    List<String> rows = List.of("continent", "country");
+    QueryDto query2 = Query
+            .from(this.storePopulation)
+            .select(tableFields(rows), measuresPop)
+            .build();
+
+    List<String> columns = List.of("spending category");
+    verifyResults(testInfo, query1, query2, JoinType.LEFT, tableFields(rows), tableFields(columns));
+  }
+
+  /**
+   * This test is using the respective {@link TableField} object. In that case, since the queries are coming from two
+   * different tables, the fields to be used in rows and columns have to be aliased fields.
+   */
+  @Test
+  void testDrillingAcrossFullName(TestInfo testInfo) {
+    Measure amount = Functions.sum("amount", this.amount);
+    Measure pop = Functions.sum("population", this.population);
+
+    List<Measure> measuresSpending = List.of(amount);
+    QueryDto query1 = Query
+            .from(this.storeSpending)
+            .select(List.of(this.spendingCategory, this.continent.as("continent"), this.country.as("country")), measuresSpending)
+            .build();
+
     QueryDto query2 = Query
             .from(this.storePopulation)
-            .select(tableFields(List.of("continent", "country")), measuresPop)
+            .select(List.of(this.continentPop.as("continent"), this.countryPop.as("country")), List.of(pop))
             .build();
 
-    verifyResults(testInfo, query1, query2, JoinType.LEFT, List.of("continent", "country"), List.of("spending category"));
+    List<Field> rows = List.of(new AliasedField("continent"), new AliasedField("country"));
+    List<Field> columns = List.of(this.spendingCategory);
+
+    verifyResults(testInfo, query1, query2, JoinType.LEFT, rows, columns);
   }
 
   @Test
@@ -391,21 +469,21 @@ public abstract class ATestPivotTable extends ABaseTestQuery {
     | Grand Total |         Grand Total |        null |       465.0 |   null |       70.0 |   null |       65.0 |   null |      330.0 |
     +-------------+---------------------+-------------+-------------+--------+------------+--------+------------+--------+------------+
      */
-    verifyResults(testInfo, query1, query2, JoinType.INNER, List.of("group", "spending category"), List.of("country"));
+    verifyResults(testInfo, query1, query2, JoinType.INNER, tableFields(List.of("group", "spending category")), tableFields(List.of("country")));
   }
 
   private void verifyResults(TestInfo testInfo, QueryDto query, List<String> rows, List<String> columns) {
-    verifyResults(testInfo, query, null, null, rows, columns);
+    verifyResults(testInfo, query, null, null, tableFields(rows), tableFields(columns));
   }
 
   /**
    * 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, QueryDto query1, QueryDto query2, JoinType joinType, List<String> rows, List<String> columns) {
+  private void verifyResults(TestInfo testInfo, QueryDto query1, QueryDto query2, JoinType joinType, List<Field> rows, List<Field> columns) {
     PivotTable pt = query2 == null
-            ? this.executor.executePivotQuery(new PivotTableQueryDto(query1, tableFields(rows), tableFields(columns)))
-            : this.executor.executePivotQueryMerge(QueryMergeDto.from(query1).join(query2, joinType), tableFields(rows), tableFields(columns), null);
+            ? this.executor.executePivotQuery(new PivotTableQueryDto(query1, rows, columns, false))
+            : this.executor.executePivotQueryMerge(new PivotTableQueryMergeDto(QueryMergeDto.from(query1).join(query2, joinType), rows, columns, false), null);
     Table expectedTabular = tableFromFile(testInfo);
 
     Assertions.assertThat(pt.table).containsExactlyElementsOf(ImmutableList.copyOf(expectedTabular.iterator()));
diff --git a/core/src/test/java/io/squashql/query/ATestQueryCache.java b/core/src/test/java/io/squashql/query/ATestQueryCache.java
index cb8e87e..0b5ca1a 100644
--- a/core/src/test/java/io/squashql/query/ATestQueryCache.java
+++ b/core/src/test/java/io/squashql/query/ATestQueryCache.java
@@ -506,16 +506,16 @@ public abstract class ATestQueryCache extends ABaseTestQuery {
             .select(List.of(category, ean), List.of(sum("ca", "price")))
             .build();
     int base = 0;
-    this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(category), List.of(ean)));
+    this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(category), List.of(ean), false));
     assertCacheStats(0, (base = base + 2));
-    this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(category, ean), List.of()));
+    this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(category, ean), List.of(), false));
     assertCacheStats(0, (base = base + 2));
-    this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(), List.of(category, ean)));
+    this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(), List.of(category, ean), false));
     assertCacheStats(2, base); // same as the previous
-    this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(ean), List.of(category)));
+    this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(ean), List.of(category), false));
     assertCacheStats(2, (base = base + 2));
     // Same as the first query
-    this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(category), List.of(ean)));
+    this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(category), List.of(ean), false));
     assertCacheStats(4, base);
   }
 
diff --git a/core/src/test/java/io/squashql/query/ATestVectorAggregation.java b/core/src/test/java/io/squashql/query/ATestVectorAggregation.java
index a94b881..a76b82e 100644
--- a/core/src/test/java/io/squashql/query/ATestVectorAggregation.java
+++ b/core/src/test/java/io/squashql/query/ATestVectorAggregation.java
@@ -386,7 +386,7 @@ public abstract class ATestVectorAggregation extends ABaseTestQuery {
             .select(List.of(ean, competitor), List.of(vector))
             .build();
 
-    PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(query, List.of(competitor), List.of(ean)));
+    PivotTable result = this.executor.executePivotQuery(new PivotTableQueryDto(query, List.of(competitor), List.of(ean), false));
     Assertions.assertThat(result.table.headers().stream().map(Header::name))
             .containsExactly(ean.name(), competitor.name(), vector.alias());
     List<List<Object>> points = List.of(
diff --git a/core/src/test/java/io/squashql/template/ClassTemplateGenerator.java b/core/src/test/java/io/squashql/template/ClassTemplateGenerator.java
index d00e74b..0f37810 100644
--- a/core/src/test/java/io/squashql/template/ClassTemplateGenerator.java
+++ b/core/src/test/java/io/squashql/template/ClassTemplateGenerator.java
@@ -68,7 +68,7 @@ public class ClassTemplateGenerator {
       }
       writer.flush();
     }
-    System.out.println("Class generated: ");
+    System.out.println("Class generated (" + classGenerated.size() + ")");
     classGenerated.forEach(System.out::println);
   }
 }
diff --git a/core/src/test/resources/queryresults/pivottable/testDrillingAcrossFullName.pivottable.json b/core/src/test/resources/queryresults/pivottable/testDrillingAcrossFullName.pivottable.json
new file mode 100644
index 0000000..7fe2cfc
--- /dev/null
+++ b/core/src/test/resources/queryresults/pivottable/testDrillingAcrossFullName.pivottable.json
@@ -0,0 +1,82 @@
+[
+  [
+    "storespendingatestpivottable.spending category",
+    "storespendingatestpivottable.spending category",
+    "Grand Total",
+    "Grand Total",
+    "extra",
+    "extra",
+    "minimum expenditure",
+    "minimum expenditure"
+  ],
+  [
+    "continent",
+    "country",
+    "amount",
+    "population",
+    "amount",
+    "population",
+    "amount",
+    "population"
+  ],
+  [
+    "Grand Total",
+    "Grand Total",
+    56.0,
+    465.0,
+    17.0,
+    null,
+    39.0,
+    null
+  ],
+  [
+    "am",
+    "Total",
+    39.0,
+    330.0,
+    10.0,
+    null,
+    29.0,
+    null
+  ],
+  [
+    "am",
+    "usa",
+    39.0,
+    330.0,
+    10.0,
+    null,
+    29.0,
+    null
+  ],
+  [
+    "eu",
+    "Total",
+    17.0,
+    135.0,
+    7.0,
+    null,
+    10.0,
+    null
+  ],
+  [
+    "eu",
+    "france",
+    8.0,
+    70.0,
+    2.0,
+    null,
+    6.0,
+    null
+  ],
+  [
+    "eu",
+    "uk",
+    9.0,
+    65.0,
+    5.0,
+    null,
+    4.0,
+    null
+  ]
+]
diff --git a/core/src/test/resources/queryresults/pivottable/testDrillingAcrossFullName.tabular.json b/core/src/test/resources/queryresults/pivottable/testDrillingAcrossFullName.tabular.json
new file mode 100644
index 0000000..a454e3e
--- /dev/null
+++ b/core/src/test/resources/queryresults/pivottable/testDrillingAcrossFullName.tabular.json
@@ -0,0 +1,157 @@
+{
+  "rows":[
+    [
+      "Grand Total",
+      "Grand Total",
+      "Grand Total",
+      56.0,
+      465.0
+    ],
+    [
+      "Grand Total",
+      "am",
+      "Total",
+      39.0,
+      330.0
+    ],
+    [
+      "Grand Total",
+      "am",
+      "usa",
+      39.0,
+      330.0
+    ],
+    [
+      "Grand Total",
+      "eu",
+      "Total",
+      17.0,
+      135.0
+    ],
+    [
+      "Grand Total",
+      "eu",
+      "france",
+      8.0,
+      70.0
+    ],
+    [
+      "Grand Total",
+      "eu",
+      "uk",
+      9.0,
+      65.0
+    ],
+    [
+      "extra",
+      "Grand Total",
+      "Grand Total",
+      17.0,
+      null
+    ],
+    [
+      "extra",
+      "am",
+      "Total",
+      10.0,
+      null
+    ],
+    [
+      "extra",
+      "am",
+      "usa",
+      10.0,
+      null
+    ],
+    [
+      "extra",
+      "eu",
+      "Total",
+      7.0,
+      null
+    ],
+    [
+      "extra",
+      "eu",
+      "france",
+      2.0,
+      null
+    ],
+    [
+      "extra",
+      "eu",
+      "uk",
+      5.0,
+      null
+    ],
+    [
+      "minimum expenditure",
+      "Grand Total",
+      "Grand Total",
+      39.0,
+      null
+    ],
+    [
+      "minimum expenditure",
+      "am",
+      "Total",
+      29.0,
+      null
+    ],
+    [
+      "minimum expenditure",
+      "am",
+      "usa",
+      29.0,
+      null
+    ],
+    [
+      "minimum expenditure",
+      "eu",
+      "Total",
+      10.0,
+      null
+    ],
+    [
+      "minimum expenditure",
+      "eu",
+      "france",
+      6.0,
+      null
+    ],
+    [
+      "minimum expenditure",
+      "eu",
+      "uk",
+      4.0,
+      null
+    ]
+  ],
+  "headers":[
+    {
+      "name":"storespendingatestpivottable.spending category",
+      "type":"java.lang.String",
+      "isMeasure":false
+    },
+    {
+      "name":"continent",
+      "type":"java.lang.String",
+      "isMeasure":false
+    },
+    {
+      "name":"country",
+      "type":"java.lang.String",
+      "isMeasure":false
+    },
+    {
+      "name":"amount",
+      "type":"double",
+      "isMeasure":true
+    },
+    {
+      "name":"population",
+      "type":"double",
+      "isMeasure":true
+    }
+  ]
+}
diff --git a/duckdb/src/test/java/io/squashql/TestDuckDBDoc.java b/duckdb/src/test/java/io/squashql/TestDuckDBDoc.java
index 8ba1e1b..a86d38f 100644
--- a/duckdb/src/test/java/io/squashql/TestDuckDBDoc.java
+++ b/duckdb/src/test/java/io/squashql/TestDuckDBDoc.java
@@ -1,18 +1,16 @@
 package io.squashql;
 
-import com.google.common.collect.ImmutableList;
 import io.squashql.jackson.JacksonUtil;
 import io.squashql.query.*;
 import io.squashql.query.builder.Query;
 import io.squashql.query.database.DuckDBQueryEngine;
 import io.squashql.query.dto.*;
 import io.squashql.table.PivotTable;
-import io.squashql.table.TableUtils;
+import io.squashql.table.PivotTableUtils;
 import org.junit.jupiter.api.Test;
 
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 import static io.squashql.query.ComparisonMethod.ABSOLUTE_DIFFERENCE;
 import static io.squashql.query.ComparisonMethod.DIVIDE;
@@ -116,7 +114,8 @@ public class TestDuckDBDoc {
       PivotTable pt = this.executor.executePivotQuery(new PivotTableQueryDto(
               query,
               List.of(new TableField("salaryRange.label")),
-              List.of(new TableField("ageRange.label"))));
+              List.of(new TableField("ageRange.label")),
+              false));
       toJson(pt);
     }
   }
@@ -164,7 +163,8 @@ public class TestDuckDBDoc {
     PivotTable pt = this.executor.executePivotQuery(new PivotTableQueryDto(
             query,
             List.of(new TableField("product"), new TableField("region")),
-            List.of(new TableField("month"))));
+            List.of(new TableField("month")),
+            false));
     pt.show();
     toJson(pt);
   }
@@ -253,7 +253,8 @@ public class TestDuckDBDoc {
     PivotTable pt = this.executor.executePivotQuery(new PivotTableQueryDto(
             query,
             fields,
-            List.of()));
+            List.of(),
+            false));
     pt.show();
     toJson(pt);
   }
@@ -299,7 +300,8 @@ public class TestDuckDBDoc {
     PivotTable pt = this.executor.executePivotQuery(new PivotTableQueryDto(
             query,
             fields,
-            List.of()));
+            List.of(),
+            false));
     pt.show();
     toJson(pt);
   }
@@ -308,15 +310,7 @@ public class TestDuckDBDoc {
    * Json output for jsfiddle. Example: <a href="https://jsfiddle.net/azeq/dqebkp2x/">https://jsfiddle.net/azeq/dqebkp2x/</a>
    */
   private static void toJson(PivotTable pt) {
-    List<String> fields = pt.table.headers().stream().map(Header::name).collect(Collectors.toList());
-    SimpleTableDto simpleTable = SimpleTableDto.builder()
-            .rows(ImmutableList.copyOf(pt.table.iterator()))
-            .columns(fields)
-            .build();
-    QueryResultDto result = QueryResultDto.builder()
-            .table(simpleTable)
-            .metadata(TableUtils.buildTableMetadata(pt.table))
-            .build();
-    System.out.println(JacksonUtil.serialize(new PivotTableQueryResultDto(result, pt.rows, pt.columns, pt.values)));
+    List<Map<String, Object>> cells = PivotTableUtils.generateCells(pt, null);
+    System.out.println(JacksonUtil.serialize(new PivotTableQueryResultDto(cells, pt.rows, pt.columns, pt.values)));
   }
 }
diff --git a/duckdb/src/test/java/io/squashql/TestDuckDBQuery.java b/duckdb/src/test/java/io/squashql/TestDuckDBQuery.java
index 7a77a00..2ac88ea 100644
--- a/duckdb/src/test/java/io/squashql/TestDuckDBQuery.java
+++ b/duckdb/src/test/java/io/squashql/TestDuckDBQuery.java
@@ -3,10 +3,7 @@ package io.squashql;
 import io.squashql.query.QueryExecutor;
 import io.squashql.query.builder.Query;
 import io.squashql.query.database.DuckDBQueryEngine;
-import io.squashql.query.dto.CacheStatsDto;
-import io.squashql.query.dto.JoinType;
-import io.squashql.query.dto.QueryDto;
-import io.squashql.query.dto.QueryMergeDto;
+import io.squashql.query.dto.*;
 import io.squashql.query.exception.LimitExceedException;
 import io.squashql.transaction.DuckDBDataLoader;
 import io.squashql.type.TableTypedField;
@@ -95,7 +92,7 @@ public class TestDuckDBQuery {
     TestUtil.assertThatThrownBy(() -> this.executor.executeQueryMerge(queryMergeDto, null))
             .isInstanceOf(LimitExceedException.class)
             .hasMessageContaining("too big");
-    TestUtil.assertThatThrownBy(() -> this.executor.executePivotQueryMerge(queryMergeDto, List.of(tableField("eanId")), List.of(), null))
+    TestUtil.assertThatThrownBy(() -> this.executor.executePivotQueryMerge(new PivotTableQueryMergeDto(queryMergeDto, List.of(tableField("eanId")), List.of(), false), null))
             .isInstanceOf(LimitExceedException.class)
             .hasMessageContaining("too big");
   }
diff --git a/duckdb/src/test/java/io/squashql/TestPivotTable.java b/duckdb/src/test/java/io/squashql/TestPivotTable.java
new file mode 100644
index 0000000..073ecb4
--- /dev/null
+++ b/duckdb/src/test/java/io/squashql/TestPivotTable.java
@@ -0,0 +1,338 @@
+package io.squashql;
+
+import io.squashql.jackson.JacksonUtil;
+import io.squashql.query.*;
+import io.squashql.query.builder.Query;
+import io.squashql.query.database.DuckDBQueryEngine;
+import io.squashql.query.dto.JoinType;
+import io.squashql.query.dto.PivotTableQueryMergeDto;
+import io.squashql.query.dto.QueryDto;
+import io.squashql.query.dto.QueryMergeDto;
+import io.squashql.table.PivotTable;
+import io.squashql.table.PivotTableUtils;
+import io.squashql.transaction.DuckDBDataLoader;
+import io.squashql.type.TableTypedField;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+public class TestPivotTable {
+
+  protected DuckDBDatastore datastore;
+  protected DuckDBQueryEngine queryEngine;
+  protected DuckDBDataLoader dl;
+  protected QueryExecutor executor;
+
+  private final String storeSpending = "store_spending";
+  private final String storePopulation = "store_population";
+  private final TableField city = new TableField(this.storeSpending, "city");
+  private final TableField country = new TableField(this.storeSpending, "country");
+  private final TableField continent = new TableField(this.storeSpending, "continent");
+  private final TableField spendingCategory = new TableField(this.storeSpending, "spending category");
+  private final TableField spendingSubcategory = new TableField(this.storeSpending, "spending subcategory");
+  private final TableField amount = new TableField(this.storeSpending, "amount");
+  private final TableField population = new TableField(this.storePopulation, "population");
+  private final TableField countryPop = new TableField(this.storePopulation, "country");
+  private final TableField continentPop = new TableField(this.storePopulation, "continent");
+
+  void setup(Map<String, List<TableTypedField>> fieldsByStore, Runnable dataLoading) {
+    this.datastore = new DuckDBDatastore();
+    this.dl = new DuckDBDataLoader(this.datastore);
+    fieldsByStore.forEach(this.dl::createOrReplaceTable);
+    this.queryEngine = new DuckDBQueryEngine(this.datastore);
+    this.executor = new QueryExecutor(this.queryEngine);
+    dataLoading.run();
+  }
+
+  protected Map<String, List<TableTypedField>> getFieldsByStore() {
+    TableTypedField city = new TableTypedField(this.storeSpending, "city", String.class);
+    TableTypedField country = new TableTypedField(this.storeSpending, "country", String.class);
+    TableTypedField continent = new TableTypedField(this.storeSpending, "continent", String.class);
+    TableTypedField spendingCategory = new TableTypedField(this.storeSpending, "spending category", String.class);
+    TableTypedField spendingSubcategory = new TableTypedField(this.storeSpending, "spending subcategory", String.class);
+    TableTypedField amount = new TableTypedField(this.storeSpending, "amount", double.class);
+    TableTypedField population = new TableTypedField(this.storePopulation, "population", double.class);
+    TableTypedField countryPop = new TableTypedField(this.storePopulation, "country", String.class);
+    TableTypedField continentPop = new TableTypedField(this.storePopulation, "continent", String.class);
+    return Map.of(
+            this.storeSpending, List.of(city, country, continent, spendingCategory, spendingSubcategory, amount),
+            this.storePopulation, List.of(countryPop, continentPop, population));
+  }
+
+  protected void loadData() {
+    this.dl.load(this.storeSpending, List.of(
+            new Object[]{"paris", "france", "eu", "minimum expenditure", "car", 1d},
+            new Object[]{"paris", "france", "eu", "minimum expenditure", "housing", 2d},
+            new Object[]{"paris", "france", "eu", "extra", "hobbies", 1d},
+            new Object[]{"lyon", "france", "eu", "minimum expenditure", "car", 1d},
+            new Object[]{"lyon", "france", "eu", "minimum expenditure", "housing", 2d},
+            new Object[]{"lyon", "france", "eu", "extra", "hobbies", 1d},
+            new Object[]{"london", "uk", "eu", "minimum expenditure", "car", 2d},
+            new Object[]{"london", "uk", "eu", "minimum expenditure", "housing", 2d},
+            new Object[]{"london", "uk", "eu", "extra", "hobbies", 5d},
+
+            new Object[]{"nyc", "usa", "am", "minimum expenditure", "car", 8d},
+            new Object[]{"nyc", "usa", "am", "minimum expenditure", "housing", 12d},
+            new Object[]{"nyc", "usa", "am", "extra", "hobbies", 6d},
+
+            new Object[]{"la", "usa", "am", "minimum expenditure", "car", 7d},
+            new Object[]{"la", "usa", "am", "minimum expenditure", "housing", 2d},
+            new Object[]{"la", "usa", "am", "extra", "hobbies", 4d}
+    ));
+
+
+    this.dl.load(this.storePopulation, List.of(
+            new Object[]{"france", "eu", 70},
+            new Object[]{"uk", "eu", 65},
+            new Object[]{"usa", "am", 330}
+    ));
+  }
+
+  @Test
+  void testDrillingAcrossFullName() {
+    setup(getFieldsByStore(), this::loadData);
+
+    Measure amount = Functions.sum("amount", this.amount);
+    Measure pop = Functions.sum("population", this.population);
+
+    List<Measure> measuresSpending = List.of(amount);
+    QueryDto query1 = Query
+            .from(this.storeSpending)
+            .select(List.of(this.spendingCategory.as("category"), this.continent.as("continent"), this.country.as("country")), measuresSpending)
+            .build();
+
+    QueryDto query2 = Query
+            .from(this.storePopulation)
+            .select(List.of(this.continentPop.as("continent"), this.countryPop.as("country")), List.of(pop))
+            .build();
+
+    QueryMergeDto queryMerge = QueryMergeDto.from(query1).join(query2, JoinType.LEFT);
+    List<Field> rows = List.of(new AliasedField("continent"), new AliasedField("country"));
+    List<Field> columns = List.of(this.spendingCategory.as("category"));
+
+    PivotTable pivotTable = this.executor.executePivotQueryMerge(new PivotTableQueryMergeDto(queryMerge, rows, columns, true), null);
+    {
+      List<Map<String, Object>> cells = PivotTableUtils.generateCells(pivotTable, true);
+      String expectedCells = """
+            [
+                          {
+                            "amount": 56.0,
+                            "population": 465.0
+                          },
+                          {
+                            "continent": "am",
+                            "amount": 39.0,
+                            "population": 330.0
+                          },
+                          {
+                            "continent": "am",
+                            "country": "usa",
+                            "amount": 39.0,
+                            "population": 330.0
+                          },
+                          {
+                            "continent": "eu",
+                            "amount": 17.0,
+                            "population": 135.0
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "france",
+                            "amount": 8.0,
+                            "population": 70.0
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "uk",
+                            "amount": 9.0,
+                            "population": 65.0
+                          },
+                          {
+                            "amount": 17.0,
+                            "category": "extra"
+                          },
+                          {
+                            "continent": "am",
+                            "amount": 10.0,
+                            "category": "extra"
+                          },
+                          {
+                            "continent": "am",
+                            "country": "usa",
+                            "amount": 10.0,
+                            "category": "extra"
+                          },
+                          {
+                            "continent": "eu",
+                            "amount": 7.0,
+                            "category": "extra"
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "france",
+                            "amount": 2.0,
+                            "category": "extra"
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "uk",
+                            "amount": 5.0,
+                            "category": "extra"
+                          },
+                          {
+                            "amount": 39.0,
+                            "category": "minimum expenditure"
+                          },
+                          {
+                            "continent": "am",
+                            "amount": 29.0,
+                            "category": "minimum expenditure"
+                          },
+                          {
+                            "continent": "am",
+                            "country": "usa",
+                            "amount": 29.0,
+                            "category": "minimum expenditure"
+                          },
+                          {
+                            "continent": "eu",
+                            "amount": 10.0,
+                            "category": "minimum expenditure"
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "france",
+                            "amount": 6.0,
+                            "category": "minimum expenditure"
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "uk",
+                            "amount": 4.0,
+                            "category": "minimum expenditure"
+                          }
+                        ]
+                        """;
+      Assertions.assertThat(cells).isEqualTo(JacksonUtil.deserialize(expectedCells, List.class));
+    }
+
+    {
+      List<Map<String, Object>> cells = PivotTableUtils.generateCells(pivotTable, false);
+      String expectedCells = """
+            [
+                          {
+                            "amount": 56.0,
+                            "population": 465.0
+                          },
+                          {
+                            "continent": "am",
+                            "amount": 39.0,
+                            "population": 330.0
+                          },
+                          {
+                            "continent": "am",
+                            "country": "usa",
+                            "amount": 39.0,
+                            "population": 330.0
+                          },
+                          {
+                            "continent": "eu",
+                            "amount": 17.0,
+                            "population": 135.0
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "france",
+                            "amount": 8.0,
+                            "population": 70.0
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "uk",
+                            "amount": 9.0,
+                            "population": 65.0
+                          },
+                          {
+                            "amount": 17.0,
+                            "category": "extra",
+                            "population": null
+                          },
+                          {
+                            "continent": "am",
+                            "amount": 10.0,
+                            "category": "extra",
+                            "population": null
+                          },
+                          {
+                            "continent": "am",
+                            "country": "usa",
+                            "amount": 10.0,
+                            "category": "extra",
+                            "population": null
+                          },
+                          {
+                            "continent": "eu",
+                            "amount": 7.0,
+                            "category": "extra",
+                            "population": null
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "france",
+                            "amount": 2.0,
+                            "category": "extra",
+                            "population": null
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "uk",
+                            "amount": 5.0,
+                            "category": "extra",
+                            "population": null
+                          },
+                          {
+                            "amount": 39.0,
+                            "category": "minimum expenditure",
+                            "population": null
+                          },
+                          {
+                            "continent": "am",
+                            "amount": 29.0,
+                            "category": "minimum expenditure",
+                            "population": null
+                          },
+                          {
+                            "continent": "am",
+                            "country": "usa",
+                            "amount": 29.0,
+                            "category": "minimum expenditure",
+                            "population": null
+                          },
+                          {
+                            "continent": "eu",
+                            "amount": 10.0,
+                            "category": "minimum expenditure",
+                            "population": null
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "france",
+                            "amount": 6.0,
+                            "category": "minimum expenditure",
+                            "population": null
+                          },
+                          {
+                            "continent": "eu",
+                            "country": "uk",
+                            "amount": 4.0,
+                            "category": "minimum expenditure",
+                            "population": null
+                          }
+                        ]
+                        """;
+      Assertions.assertThat(cells).isEqualTo(JacksonUtil.deserialize(expectedCells, List.class));
+    }
+  }
+}
diff --git a/server/src/test/java/io/squashql/client/HttpClientQuerierTest.java b/server/src/test/java/io/squashql/client/HttpClientQuerierTest.java
index 19bfe63..d6e57cc 100644
--- a/server/src/test/java/io/squashql/client/HttpClientQuerierTest.java
+++ b/server/src/test/java/io/squashql/client/HttpClientQuerierTest.java
@@ -23,7 +23,6 @@ import static io.squashql.query.Functions.criterion;
 import static io.squashql.query.Functions.eq;
 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.transaction.DataLoader.MAIN_SCENARIO_NAME;
 import static io.squashql.transaction.DataLoader.SCENARIO_FIELD_NAME;
 
@@ -184,24 +183,23 @@ public class HttpClientQuerierTest {
     QueryDto query = Query.from("our_prices")
             .select(tableFields(List.of("ean", "pdv")), List.of(CountMeasure.INSTANCE))
             .build();
-    PivotTableQueryDto pivotTableQuery = new PivotTableQueryDto(query, tableFields(List.of("pdv")), tableFields(List.of("ean")));
+    PivotTableQueryDto pivotTableQuery = new PivotTableQueryDto(query, tableFields(List.of("pdv")), tableFields(List.of("ean")), false);
     PivotTableQueryResultDto response = this.querier.run(pivotTableQuery);
 
     Assertions.assertThat(response.rows).containsExactlyElementsOf(pivotTableQuery.rows.stream().map(Field::name).toList());
     Assertions.assertThat(response.columns).containsExactlyElementsOf(pivotTableQuery.columns.stream().map(Field::name).toList());
     Assertions.assertThat(response.values).containsExactlyElementsOf(List.of(CountMeasure.INSTANCE.alias));
-    Assertions.assertThat(response.queryResult.table.rows)
-            .containsExactly(
-                    List.of(GRAND_TOTAL, GRAND_TOTAL, 20),
-                    List.of(GRAND_TOTAL, "ITM Balma", 10),
-                    List.of(GRAND_TOTAL, "ITM Toulouse and Drive", 10),
-                    List.of("ITMella 250g", GRAND_TOTAL, 10),
-                    List.of("ITMella 250g", "ITM Balma", 5),
-                    List.of("ITMella 250g", "ITM Toulouse and Drive", 5),
-
-                    List.of("Nutella 250g", GRAND_TOTAL, 10),
-                    List.of("Nutella 250g", "ITM Balma", 5),
-                    List.of("Nutella 250g", "ITM Toulouse and Drive", 5));
+    List<Map<String, Object>> cells = List.of(
+            Map.of(CountMeasure.ALIAS, 20),
+            Map.of(CountMeasure.ALIAS, 10, "pdv", "ITM Balma"),
+            Map.of(CountMeasure.ALIAS, 10, "pdv", "ITM Toulouse and Drive"),
+            Map.of(CountMeasure.ALIAS, 10, "ean", "ITMella 250g"),
+            Map.of(CountMeasure.ALIAS, 5, "ean", "ITMella 250g", "pdv", "ITM Balma"),
+            Map.of(CountMeasure.ALIAS, 5, "ean", "ITMella 250g", "pdv", "ITM Toulouse and Drive"),
+            Map.of(CountMeasure.ALIAS, 10, "ean", "Nutella 250g"),
+            Map.of(CountMeasure.ALIAS, 5, "ean", "Nutella 250g", "pdv", "ITM Balma"),
+            Map.of(CountMeasure.ALIAS, 5, "ean", "Nutella 250g", "pdv", "ITM Toulouse and Drive"));
+    Assertions.assertThat(response.cells).isEqualTo(cells);
   }
 
   @Test
diff --git a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
index ae90087..eea31d8 100644
--- a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
+++ b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
@@ -199,7 +199,7 @@ public class TestJavascriptLibrary {
 
     String name = "build-from-query-pivot.json"; // The content of this file is generated by the js code.
     PivotTableQueryDto qjs = JacksonUtil.deserialize(TestUtil.readAllLines(name), PivotTableQueryDto.class);
-    Assertions.assertThat(qjs).isEqualTo(new PivotTableQueryDto(q, tableFields(List.of("a")), tableFields(List.of("b"))));
+    Assertions.assertThat(qjs).isEqualTo(new PivotTableQueryDto(q, tableFields(List.of("a")), tableFields(List.of("b")), true));
   }
 
   @Test
@@ -215,7 +215,7 @@ public class TestJavascriptLibrary {
             .build();
 
     QueryMergeDto query = QueryMergeDto.from(q1).join(q2, JoinType.LEFT);
-    PivotTableQueryMergeDto pivotTableQueryMergeDto = new PivotTableQueryMergeDto(query, tableFields(List.of("a")), tableFields(List.of("b")));
+    PivotTableQueryMergeDto pivotTableQueryMergeDto = new PivotTableQueryMergeDto(query, tableFields(List.of("a")), tableFields(List.of("b")), false);
 
     String name = "build-from-query-merge-pivot.json"; // The content of this file is generated by the js code.
     PivotTableQueryMergeDto qjs = JacksonUtil.deserialize(TestUtil.readAllLines(name), PivotTableQueryMergeDto.class);
diff --git a/server/src/test/java/io/squashql/spring/dataset/DatasetTestConfig.java b/server/src/test/java/io/squashql/spring/dataset/DatasetTestConfig.java
index 6a7cf10..cd97a52 100644
--- a/server/src/test/java/io/squashql/spring/dataset/DatasetTestConfig.java
+++ b/server/src/test/java/io/squashql/spring/dataset/DatasetTestConfig.java
@@ -46,7 +46,7 @@ public class DatasetTestConfig {
     QueryDto query = Query.from("our_prices")
             .select(tableFields(List.of("ean", "pdv", "scenario")), List.of(new AggregatedMeasure("count", "*", "count")))
             .build();
-    PivotTable pt = queryExecutor.executePivotQuery(new PivotTableQueryDto(query, tableFields(List.of("pdv", "ean")), tableFields(List.of("scenario"))));
+    PivotTable pt = queryExecutor.executePivotQuery(new PivotTableQueryDto(query, tableFields(List.of("pdv", "ean")), tableFields(List.of("scenario")), false));
     pt.show();
     toJson(pt);
   }
diff --git a/server/src/test/java/io/squashql/spring/web/rest/QueryControllerTest.java b/server/src/test/java/io/squashql/spring/web/rest/QueryControllerTest.java
index b2f631c..2b6163a 100644
--- a/server/src/test/java/io/squashql/spring/web/rest/QueryControllerTest.java
+++ b/server/src/test/java/io/squashql/spring/web/rest/QueryControllerTest.java
@@ -4,10 +4,8 @@ import io.squashql.jackson.JacksonUtil;
 import io.squashql.query.*;
 import io.squashql.query.builder.Query;
 import io.squashql.query.database.DuckDBQueryEngine;
-import io.squashql.query.database.QueryEngine;
 import io.squashql.query.dto.*;
 import io.squashql.query.exception.LimitExceedException;
-import io.squashql.query.dto.QueryJoinDto;
 import io.squashql.spring.dataset.DatasetTestConfig;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -24,7 +22,8 @@ import java.util.Map;
 import java.util.function.BiConsumer;
 import java.util.function.Function;
 
-import static io.squashql.query.Functions.*;
+import static io.squashql.query.Functions.criterion;
+import static io.squashql.query.Functions.sum;
 import static io.squashql.query.TableField.tableField;
 import static io.squashql.query.TableField.tableFields;
 import static io.squashql.transaction.DataLoader.MAIN_SCENARIO_NAME;
@@ -269,16 +268,16 @@ public class QueryControllerTest {
             .build();
 
     this.mvc.perform(MockMvcRequestBuilders.post(QueryController.MAPPING_QUERY_MERGE_PIVOT)
-                    .content(JacksonUtil.serialize(new PivotTableQueryMergeDto(QueryMergeDto.from(query1).join(query2, JoinType.FULL), tableFields(List.of("ean")), List.of())))
+                    .content(JacksonUtil.serialize(new PivotTableQueryMergeDto(QueryMergeDto.from(query1).join(query2, JoinType.FULL), tableFields(List.of("ean")), List.of(), false)))
                     .contentType(MediaType.APPLICATION_JSON))
             .andExpect(result -> {
               String contentAsString = result.getResponse().getContentAsString();
               PivotTableQueryResultDto queryResult = JacksonUtil.deserialize(contentAsString, PivotTableQueryResultDto.class);
-              Assertions.assertThat(queryResult.queryResult.table.rows).containsExactlyInAnyOrder(
-                      List.of(QueryEngine.GRAND_TOTAL, 102000d * 2, 10200d),
-                      List.of("ITMella 250g", 102000d, 10200d),
-                      List.of("Nutella 250g", 102000d, 10200d));
-              Assertions.assertThat(queryResult.queryResult.table.columns).containsExactly("ean", "capdv-sum", "capdv-avg");
+              List<Map<String, Object>> cells = List.of(
+                      Map.of("capdv-avg", 10200d, "capdv-sum", 102000d * 2),
+                      Map.of("ean", "ITMella 250g", "capdv-avg", 10200d, "capdv-sum", 102000d),
+                      Map.of("ean", "Nutella 250g", "capdv-avg", 10200d, "capdv-sum", 102000d));
+              Assertions.assertThat(queryResult.cells).isEqualTo(cells);
             });
   }
 
diff --git a/server/src/test/resources/build-from-query-pivot.json b/server/src/test/resources/build-from-query-pivot.json
index d1dcdcf..c6da9bd 100644
--- a/server/src/test/resources/build-from-query-pivot.json
+++ b/server/src/test/resources/build-from-query-pivot.json
@@ -1,1 +1,1 @@
-{"query":{"table":{"name":"myTable","joins":[]},"virtualTableDtos":[],"columns":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"},{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f1","fieldName":"f1"},"aggregationFunction":"avg","alias":"sum"}],"orders":{},"limit":-1},"rows":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"}],"columns":[{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}]}
\ No newline at end of file
+{"query":{"table":{"name":"myTable","joins":[]},"virtualTableDtos":[],"columns":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"},{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f1","fieldName":"f1"},"aggregationFunction":"avg","alias":"sum"}],"orders":{},"limit":-1},"rows":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"}],"columns":[{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}],"minify":true}
\ No newline at end of file
diff --git a/snowflake/src/test/java/io/squashql/query/TestSnowflakePivotTable.java b/snowflake/src/test/java/io/squashql/query/TestSnowflakePivotTable.java
new file mode 100644
index 0000000..998023f
--- /dev/null
+++ b/snowflake/src/test/java/io/squashql/query/TestSnowflakePivotTable.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 TestSnowflakePivotTable extends ATestPivotTable {
+
+  @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);
+  }
+}