Repository: squashql/squashql
Pull request #242 (2024-01-17 08:19:51)

Handle multiple virtual tables
Fixes #237 #238 


Patch:
diff --git a/core/src/main/java/io/squashql/query/MeasureUtils.java b/core/src/main/java/io/squashql/query/MeasureUtils.java
index fe56d03..d2692a0 100644
--- a/core/src/main/java/io/squashql/query/MeasureUtils.java
+++ b/core/src/main/java/io/squashql/query/MeasureUtils.java
@@ -110,7 +110,7 @@ public final class MeasureUtils {
             queryScope.havingCriteria(),
             new ArrayList<>(rollupColumns),
             new ArrayList<>(queryScope.groupingSets()), // FIXME should handle groupingSets
-            queryScope.virtualTable(),
+            queryScope.virtualTables(),
             queryScope.limit());
   }
 
diff --git a/core/src/main/java/io/squashql/query/QueryExecutor.java b/core/src/main/java/io/squashql/query/QueryExecutor.java
index 7f46ae2..93185ae 100644
--- a/core/src/main/java/io/squashql/query/QueryExecutor.java
+++ b/core/src/main/java/io/squashql/query/QueryExecutor.java
@@ -246,7 +246,7 @@ public class QueryExecutor {
 
   private static boolean canBeCached(CompiledMeasure measure, QueryScope scope) {
     // Make sure to never cache the grouping measures. It could cause issue in some cases.
-    if(generateGroupingMeasures(scope).values().contains(measure)) {
+    if (generateGroupingMeasures(scope).values().contains(measure)) {
       return false;
     }
     // In case of vectors, we can rely only on the alias of the measure.
@@ -287,7 +287,7 @@ public class QueryExecutor {
                            CompiledCriteria havingCriteria,
                            List<TypedField> rollupColumns,
                            List<List<TypedField>> groupingSets,
-                           VirtualTableDto virtualTable,
+                           List<VirtualTableDto> virtualTables,
                            int limit) {
 
     @Override
@@ -312,8 +312,8 @@ public class QueryExecutor {
       if (groupingSets != null && !groupingSets.isEmpty()) {
         sb.append(", groupingSets=").append(groupingSets);
       }
-      if (virtualTable != null) {
-        sb.append(", virtualTable=").append(virtualTable);
+      if (virtualTables != null && !virtualTables.isEmpty()) {
+        sb.append(", virtualTables=").append(virtualTables);
       }
       if (limit > 0) {
         sb.append(", limit=").append(limit);
diff --git a/core/src/main/java/io/squashql/query/QueryResolver.java b/core/src/main/java/io/squashql/query/QueryResolver.java
index 3b51828..9450671 100644
--- a/core/src/main/java/io/squashql/query/QueryResolver.java
+++ b/core/src/main/java/io/squashql/query/QueryResolver.java
@@ -34,8 +34,10 @@ public class QueryResolver {
   public QueryResolver(QueryDto query, Map<String, Store> storesByName) {
     this.query = query;
     this.storesByName = storesByName;
-    if (query.virtualTableDto != null) {
-      this.storesByName.put(query.virtualTableDto.name, VirtualTableDto.toStore(query.virtualTableDto));
+    if (query.virtualTableDtos != null) {
+      for (VirtualTableDto virtualTableDto : query.virtualTableDtos) {
+        this.storesByName.put(virtualTableDto.name, VirtualTableDto.toStore(virtualTableDto));
+      }
     }
     this.columns = query.columns.stream().map(this::resolveField).toList();
     this.bucketColumns = Optional.ofNullable(query.columnSets.get(ColumnSetKey.BUCKET))
@@ -154,7 +156,7 @@ public class QueryResolver {
             compileCriteria(query.havingCriteriaDto),
             rollupColumns,
             groupingSets,
-            query.virtualTableDto,
+            query.virtualTableDtos,
             query.limit);
   }
 
@@ -190,8 +192,8 @@ public class QueryResolver {
     if (subQuery.subQuery != null) {
       throw new IllegalArgumentException("sub-query in a sub-query is not supported");
     }
-    if (subQuery.virtualTableDto != null) {
-      throw new IllegalArgumentException("virtualTable in a sub-query is not supported");
+    if (subQuery.virtualTableDtos != null && !subQuery.virtualTableDtos.isEmpty()) {
+      throw new IllegalArgumentException("virtualTables in a sub-query is not supported");
     }
     if (subQuery.columnSets != null && !subQuery.columnSets.isEmpty()) {
       throw new IllegalArgumentException("column sets are not expected in sub query: " + subQuery);
@@ -202,7 +204,7 @@ public class QueryResolver {
   }
 
   public DatabaseQuery toDatabaseQuery(final QueryExecutor.QueryScope query, final int limit) {
-    return new DatabaseQuery(query.virtualTable(),
+    return new DatabaseQuery(query.virtualTables(),
             query.table(),
             query.subQuery(),
             new LinkedHashSet<>(query.columns()),
diff --git a/core/src/main/java/io/squashql/query/builder/Query.java b/core/src/main/java/io/squashql/query/builder/Query.java
index 389896d..47ee7b8 100644
--- a/core/src/main/java/io/squashql/query/builder/Query.java
+++ b/core/src/main/java/io/squashql/query/builder/Query.java
@@ -3,15 +3,9 @@ package io.squashql.query.builder;
 import io.squashql.query.ColumnSet;
 import io.squashql.query.Field;
 import io.squashql.query.Measure;
-import io.squashql.query.dto.ConditionDto;
-import io.squashql.query.dto.CriteriaDto;
-import io.squashql.query.dto.JoinType;
-import io.squashql.query.dto.OrderKeywordDto;
-import io.squashql.query.dto.QueryDto;
-import io.squashql.query.dto.TableDto;
-import io.squashql.query.dto.VirtualTableDto;
+import io.squashql.query.dto.*;
+
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 
 public class Query implements HasCondition, HasHaving, HasJoin, HasStartedBuildingTable, HasOrderBy, CanAddRollup {
@@ -42,7 +36,7 @@ public class Query implements HasCondition, HasHaving, HasJoin, HasStartedBuildi
   @Override
   public HasStartedBuildingJoin join(VirtualTableDto virtualTableDto, JoinType joinType) {
     addJoinToQueryDto();
-    this.queryDto.virtualTableDto = virtualTableDto;
+    this.queryDto.virtualTableDtos.add(virtualTableDto);
     this.currentJoinTableBuilder = new JoinTableBuilder(this, virtualTableDto.name, JoinType.INNER);
     return this.currentJoinTableBuilder;
   }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledTable.java b/core/src/main/java/io/squashql/query/compiled/CompiledTable.java
index 8548d0b..59a3280 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledTable.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledTable.java
@@ -11,10 +11,18 @@ public record CompiledTable(String name, List<CompiledJoin> joins) {
 
   public String sqlExpression(QueryRewriter queryRewriter) {
     StringBuilder statement = new StringBuilder();
-    VirtualTableDto virtualTable = queryRewriter.query() != null ? queryRewriter.query().virtualTableDto : null;
+    List<VirtualTableDto> virtualTables = queryRewriter.query() != null ? queryRewriter.query().virtualTableDtos : null;
     statement.append(queryRewriter.tableName(this.name));
-    Function<String, String> tableNameFunc = tableName -> virtualTable != null && virtualTable.name.equals(tableName)
-            ? queryRewriter.cteName(tableName) : queryRewriter.tableName(tableName);
+    Function<String, String> tableNameFunc = tableName -> {
+      if (virtualTables != null) {
+        for (VirtualTableDto virtualTable : virtualTables) {
+          if (virtualTable.name.equals(tableName)) {
+            return queryRewriter.cteName(tableName);
+          }
+        }
+      }
+      return queryRewriter.tableName(tableName);
+    };
     this.joins.forEach(j -> statement.append(j.sqlExpression(queryRewriter, tableNameFunc)));
     return statement.toString();
   }
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 2806169..07cf7f3 100644
--- a/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java
+++ b/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java
@@ -179,7 +179,7 @@ public class PrefetchVisitor implements MeasureVisitor<Map<QueryScope, Set<Compi
         subQueryMeasures.add(new CompiledAggregatedMeasure(subQueryMeasureAlias, fieldToAggregate, vectorAggFunc, null, false));
       }
 
-      DatabaseQuery subQuery = new DatabaseQuery(this.originalQueryScope.virtualTable(),
+      DatabaseQuery subQuery = new DatabaseQuery(this.originalQueryScope.virtualTables(),
               this.originalQueryScope.table(),
               this.originalQueryScope.subQuery(),
               new HashSet<>(subQuerySelectColumns),
@@ -198,7 +198,7 @@ public class PrefetchVisitor implements MeasureVisitor<Map<QueryScope, Set<Compi
               this.originalQueryScope.havingCriteria(),
               Collections.emptyList(), // remove rollup, it has been computed in the subquery
               Collections.emptyList(),
-              this.originalQueryScope.virtualTable(),
+              this.originalQueryScope.virtualTables(),
               this.originalQueryScope.limit());
 
       int size = subQueryMeasureAliases.size();
diff --git a/core/src/main/java/io/squashql/query/database/DatabaseQuery.java b/core/src/main/java/io/squashql/query/database/DatabaseQuery.java
index 31dec28..26456db 100644
--- a/core/src/main/java/io/squashql/query/database/DatabaseQuery.java
+++ b/core/src/main/java/io/squashql/query/database/DatabaseQuery.java
@@ -16,7 +16,7 @@ import java.util.Set;
 @RequiredArgsConstructor
 public class DatabaseQuery {
 
-  public final VirtualTableDto virtualTableDto; // CTE
+  public final List<VirtualTableDto> virtualTableDtos; // CTEs
   public final CompiledTable table;
   public final DatabaseQuery subQuery;
   public final Set<TypedField> select;
@@ -54,8 +54,8 @@ public class DatabaseQuery {
     if (groupingSets != null && !groupingSets.isEmpty()) {
       sb.append(", groupingSets=").append(groupingSets);
     }
-    if (virtualTableDto != null) {
-      sb.append(", virtualTable=").append(virtualTableDto);
+    if (virtualTableDtos != null && !virtualTableDtos.isEmpty()) {
+      sb.append(", virtualTable=").append(virtualTableDtos);
     }
     if (limit > 0) {
       sb.append(", limit=").append(limit);
diff --git a/core/src/main/java/io/squashql/query/database/QueryRewriter.java b/core/src/main/java/io/squashql/query/database/QueryRewriter.java
index 45c05b1..ed036ae 100644
--- a/core/src/main/java/io/squashql/query/database/QueryRewriter.java
+++ b/core/src/main/java/io/squashql/query/database/QueryRewriter.java
@@ -6,19 +6,22 @@ import io.squashql.type.FunctionTypedField;
 import io.squashql.type.TableTypedField;
 import io.squashql.type.TypedField;
 
+import java.util.List;
+
 public interface QueryRewriter {
 
   DatabaseQuery query();
 
   default String getFieldFullName(TableTypedField f) {
-    VirtualTableDto vt = query() != null ? query().virtualTableDto : null;
-    if (vt != null
-            && vt.name.equals(f.store())
-            && vt.fields.contains(f.name())) {
-      return SqlUtils.getFieldFullName(cteName(f.store()), fieldName(f.name()));
-    } else {
-      return SqlUtils.getFieldFullName(f.store() == null ? null : tableName(f.store()), fieldName(f.name()));
+    List<VirtualTableDto> vts = query() != null ? query().virtualTableDtos : null;
+    if (vts != null) {
+      for (VirtualTableDto virtualTable : vts) {
+        if (virtualTable.name.equals(f.store()) && virtualTable.fields.contains(f.name())) {
+          return SqlUtils.getFieldFullName(cteName(f.store()), fieldName(f.name()));
+        }
+      }
     }
+    return SqlUtils.getFieldFullName(f.store() == null ? null : tableName(f.store()), fieldName(f.name()));
   }
 
   default String fieldName(String field) {
diff --git a/core/src/main/java/io/squashql/query/database/SQLTranslator.java b/core/src/main/java/io/squashql/query/database/SQLTranslator.java
index ed727c0..4170fa4 100644
--- a/core/src/main/java/io/squashql/query/database/SQLTranslator.java
+++ b/core/src/main/java/io/squashql/query/database/SQLTranslator.java
@@ -32,7 +32,7 @@ public class SQLTranslator {
     selects.addAll(aggregates);
 
     StringBuilder statement = new StringBuilder();
-    addCte(query.virtualTableDto, statement, queryRewriter);
+    addCtes(query.virtualTableDtos, statement, queryRewriter);
     statement.append("select ");
     statement.append(String.join(", ", selects));
     statement.append(" from ");
@@ -54,34 +54,45 @@ public class SQLTranslator {
     return statement.toString();
   }
 
-  private static void addCte(VirtualTableDto virtualTableDto, StringBuilder statement, QueryRewriter qr) {
-    if (virtualTableDto == null) {
+  private static void addCtes(List<VirtualTableDto> virtualTableDtos, StringBuilder statement, QueryRewriter qr) {
+    if (virtualTableDtos == null || virtualTableDtos.isEmpty()) {
       return;
     }
 
-    StringBuilder sb = new StringBuilder();
-    Iterator<List<Object>> it = virtualTableDto.records.iterator();
-    while (it.hasNext()) {
-      sb.append("select ");
-      List<Object> row = it.next();
-      for (int i = 0; i < row.size(); i++) {
-        Object obj = row.get(i);
-        sb.append(obj instanceof String ? "'" : "");
-        sb.append(obj);
-        sb.append(obj instanceof String ? "'" : "");
-        sb.append(" as ").append(qr.fieldName(virtualTableDto.fields.get(i)));
-        if (i < row.size() - 1) {
-          sb.append(", ");
+    List<String> cteExpressions = new ArrayList<>(virtualTableDtos.size());
+    for (VirtualTableDto virtualTableDto : virtualTableDtos) {
+      StringBuilder sb = new StringBuilder();
+      Iterator<List<Object>> it = virtualTableDto.records.iterator();
+      while (it.hasNext()) {
+        sb.append("select ");
+        List<Object> row = it.next();
+        for (int i = 0; i < row.size(); i++) {
+          Object obj = row.get(i);
+          sb.append(obj instanceof String ? "'" : "");
+          sb.append(obj);
+          sb.append(obj instanceof String ? "'" : "");
+          sb.append(" as ").append(qr.fieldName(virtualTableDto.fields.get(i)));
+          if (i < row.size() - 1) {
+            sb.append(", ");
+          }
+        }
+        if (it.hasNext()) {
+          sb.append(" union all ");
         }
       }
-      if (it.hasNext()) {
-        sb.append(" union all ");
-      }
+      cteExpressions.add(sb.toString());
     }
 
-    statement
-            .append("with ").append(qr.cteName(virtualTableDto.name))
-            .append(" as (").append(sb).append(") ");
+    statement.append("with ");
+    for (int i = 0; i < cteExpressions.size(); i++) {
+      String exp = cteExpressions.get(i);
+      statement.append(qr.cteName(virtualTableDtos.get(i).name)).append(" as (").append(exp).append(")");
+      if (i < cteExpressions.size() - 1) {
+        statement.append(", ");
+      } else {
+        statement.append(" ");
+      }
+    }
   }
 
   public static void addLimit(int limit, StringBuilder statement) {
diff --git a/core/src/main/java/io/squashql/query/dto/QueryDto.java b/core/src/main/java/io/squashql/query/dto/QueryDto.java
index c314ec2..542c6be 100644
--- a/core/src/main/java/io/squashql/query/dto/QueryDto.java
+++ b/core/src/main/java/io/squashql/query/dto/QueryDto.java
@@ -26,7 +26,7 @@ public class QueryDto {
 
   public QueryDto subQuery;
 
-  public VirtualTableDto virtualTableDto; // Only support 1 single virtual for now
+  public List<VirtualTableDto> virtualTableDtos = new ArrayList<>();
 
   public List<Field> columns = new ArrayList<>();
 
diff --git a/js/typescript-library/build-from-query-join.json b/js/typescript-library/build-from-query-join.json
index e6ccd2f..2b7b67a 100644
--- a/js/typescript-library/build-from-query-join.json
+++ b/js/typescript-library/build-from-query-join.json
@@ -1,1 +1,1 @@
-{"first":{"table":{"name":"myTable1","joins":[]},"columns":[{"@class":"io.squashql.query.TableField","fullName":"myTable1.a","tableName":"myTable1","fieldName":"a"},{"@class":"io.squashql.query.TableField","fullName":"myTable1.b","tableName":"myTable1","fieldName":"b"},{"@class":"io.squashql.query.TableField","fullName":"myTable1.c","tableName":"myTable1","fieldName":"c"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f1","fieldName":"f1"},"aggregationFunction":"sum","alias":"sum"}],"orders":{},"limit":-1},"second":{"table":{"name":"myTable2","joins":[]},"columns":[{"@class":"io.squashql.query.TableField","fullName":"myTable2.b","tableName":"myTable2","fieldName":"b"},{"@class":"io.squashql.query.TableField","fullName":"myTable2.c","tableName":"myTable2","fieldName":"c"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f2","fieldName":"f2"},"aggregationFunction":"avg","alias":"sum"}],"orders":{},"limit":-1},"joinType":"LEFT","joinCondition":{"conditionType":"AND","children":[{"field":{"@class":"io.squashql.query.TableField","fullName":"myTable1.b","tableName":"myTable1","fieldName":"b"},"fieldOther":{"@class":"io.squashql.query.TableField","fullName":"myTable2.b","tableName":"myTable2","fieldName":"b"},"conditionType":"EQ"},{"field":{"@class":"io.squashql.query.TableField","fullName":"myTable1.c","tableName":"myTable1","fieldName":"c"},"fieldOther":{"@class":"io.squashql.query.TableField","fullName":"myTable2.c","tableName":"myTable2","fieldName":"c"},"conditionType":"EQ"}]},"orders":{"{\"@class\":\"io.squashql.query.TableField\",\"fullName\":\"myTable1.a\",\"tableName\":\"myTable1\",\"fieldName\":\"a\"}":{"@class":"io.squashql.query.dto.SimpleOrderDto","order":"ASC"}},"limit":10}
\ No newline at end of file
+{"first":{"table":{"name":"myTable1","joins":[]},"virtualTableDtos":[],"columns":[{"@class":"io.squashql.query.TableField","fullName":"myTable1.a","tableName":"myTable1","fieldName":"a"},{"@class":"io.squashql.query.TableField","fullName":"myTable1.b","tableName":"myTable1","fieldName":"b"},{"@class":"io.squashql.query.TableField","fullName":"myTable1.c","tableName":"myTable1","fieldName":"c"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f1","fieldName":"f1"},"aggregationFunction":"sum","alias":"sum"}],"orders":{},"limit":-1},"second":{"table":{"name":"myTable2","joins":[]},"virtualTableDtos":[],"columns":[{"@class":"io.squashql.query.TableField","fullName":"myTable2.b","tableName":"myTable2","fieldName":"b"},{"@class":"io.squashql.query.TableField","fullName":"myTable2.c","tableName":"myTable2","fieldName":"c"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f2","fieldName":"f2"},"aggregationFunction":"avg","alias":"sum"}],"orders":{},"limit":-1},"joinType":"LEFT","joinCondition":{"conditionType":"AND","children":[{"field":{"@class":"io.squashql.query.TableField","fullName":"myTable1.b","tableName":"myTable1","fieldName":"b"},"fieldOther":{"@class":"io.squashql.query.TableField","fullName":"myTable2.b","tableName":"myTable2","fieldName":"b"},"conditionType":"EQ"},{"field":{"@class":"io.squashql.query.TableField","fullName":"myTable1.c","tableName":"myTable1","fieldName":"c"},"fieldOther":{"@class":"io.squashql.query.TableField","fullName":"myTable2.c","tableName":"myTable2","fieldName":"c"},"conditionType":"EQ"}]},"orders":{"{\"@class\":\"io.squashql.query.TableField\",\"fullName\":\"myTable1.a\",\"tableName\":\"myTable1\",\"fieldName\":\"a\"}":{"@class":"io.squashql.query.dto.SimpleOrderDto","order":"ASC"}},"limit":10}
\ No newline at end of file
diff --git a/js/typescript-library/build-from-query-merge-pivot.json b/js/typescript-library/build-from-query-merge-pivot.json
index db506d4..df1d4e3 100644
--- a/js/typescript-library/build-from-query-merge-pivot.json
+++ b/js/typescript-library/build-from-query-merge-pivot.json
@@ -1,1 +1,1 @@
-{"query":{"first":{"table":{"name":"myTable","joins":[]},"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":"sum","alias":"sum"}],"orders":{},"limit":-1},"second":{"table":{"name":"myTable","joins":[]},"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},"joinType":"LEFT"},"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":{"first":{"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":"sum","alias":"sum"}],"orders":{},"limit":-1},"second":{"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},"joinType":"LEFT"},"rows":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"}],"columns":[{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}]}
\ No newline at end of file
diff --git a/js/typescript-library/build-from-query-merge.json b/js/typescript-library/build-from-query-merge.json
index 511326b..9034675 100644
--- a/js/typescript-library/build-from-query-merge.json
+++ b/js/typescript-library/build-from-query-merge.json
@@ -1,1 +1,1 @@
-{"first":{"table":{"name":"myTable","joins":[]},"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":"sum","alias":"sum"}],"orders":{},"limit":-1},"second":{"table":{"name":"myTable","joins":[]},"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},"joinType":"LEFT"}
\ No newline at end of file
+{"first":{"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":"sum","alias":"sum"}],"orders":{},"limit":-1},"second":{"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},"joinType":"LEFT"}
\ No newline at end of file
diff --git a/js/typescript-library/build-from-query-pivot.json b/js/typescript-library/build-from-query-pivot.json
index 5880ef6..d1dcdcf 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":[]},"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"}]}
\ No newline at end of file
diff --git a/js/typescript-library/src/generate-json-query.ts b/js/typescript-library/src/generate-json-query.ts
index c9b13f5..8dd5455 100644
--- a/js/typescript-library/src/generate-json-query.ts
+++ b/js/typescript-library/src/generate-json-query.ts
@@ -14,7 +14,8 @@ export function generateFromQuery() {
     "a": ["a1", "a2"],
     "b": ["b1", "b2"]
   }))
-  const cte = new VirtualTable("myCte", ["id", "min", "max", "other"], [[0, 0, 1, "x"], [1, 2, 3, "y"]])
+  const cte1 = new VirtualTable("myCte1", ["id", "min", "max", "other"], [[0, 0, 1, "x"], [1, 2, 3, "y"]])
+  const cte2 = new VirtualTable("myCte2", ["id", "min", "max", "other"], [[0, 4, 12, "a"], [1, 12, 25, "b"]])
   const bucketColumnSet = new BucketColumnSet(tableField("group"), tableField("scenario"), values)
   const measure = sum("sum", new TableField("f1"))
   const measureExpr = new ExpressionMeasure("sum_expr", "sum(f1)")
@@ -25,10 +26,15 @@ export function generateFromQuery() {
             criterion_(new TableField("myTable.id"), new TableField("refTable.id"), ConditionType.EQ),
             criterion_(new TableField("myTable.a"), new TableField("refTable.a"), ConditionType.EQ)
           ]))
-          .joinVirtual(cte, JoinType.INNER)
+          .joinVirtual(cte1, JoinType.INNER)
           .on(all([
-            criterion_(new TableField("myTable.value"), new TableField("myCte.min"), ConditionType.GE),
-            criterion_(new TableField("myTable.value"), new TableField("myCte.max"), ConditionType.LT)
+            criterion_(new TableField("myTable.value"), new TableField("myCte1.min"), ConditionType.GE),
+            criterion_(new TableField("myTable.value"), new TableField("myCte1.max"), ConditionType.LT)
+          ]))
+          .joinVirtual(cte2, JoinType.INNER)
+          .on(all([
+            criterion_(new TableField("myTable.value"), new TableField("myCte2.min"), ConditionType.GE),
+            criterion_(new TableField("myTable.value"), new TableField("myCte2.max"), ConditionType.LT)
           ]))
           .where(all([
             criterion(new TableField("f2"), gt(659)),
diff --git a/js/typescript-library/src/index.ts b/js/typescript-library/src/index.ts
index 7cd5282..35b5d87 100644
--- a/js/typescript-library/src/index.ts
+++ b/js/typescript-library/src/index.ts
@@ -55,4 +55,8 @@ export {
   default as Criteria
 } from "./criteria"
 
+export {
+  VirtualTable
+} from "./virtualtable"
+
 export * from "./dependencies"
diff --git a/js/typescript-library/src/query.ts b/js/typescript-library/src/query.ts
index 7d042e2..96c4810 100644
--- a/js/typescript-library/src/query.ts
+++ b/js/typescript-library/src/query.ts
@@ -35,7 +35,7 @@ export class Query {
   parameters: Map<string, Parameter>
   measures: Array<Measure>
   table: Table
-  virtualTable: VirtualTable
+  virtualTables: Array<VirtualTable>
   whereCriteria: Criteria
   havingCriteriaDto: Criteria
   orders: Map<Field, Order>
@@ -45,6 +45,7 @@ export class Query {
   constructor() {
     this.columns = []
     this.rollupColumns = []
+    this.virtualTables = []
     this.measures = []
     this.whereCriteria = undefined
     this.havingCriteriaDto = undefined
@@ -112,7 +113,7 @@ export class Query {
     return {
       "table": this.table,
       "subQuery": this.subQuery,
-      "virtualTableDto": this.virtualTable,
+      "virtualTableDtos": this.virtualTables,
       "columns": this.columns,
       "rollupColumns": this.rollupColumns,
       "columnSets": Object.fromEntries(this.columnSets),
diff --git a/js/typescript-library/src/queryBuilder.ts b/js/typescript-library/src/queryBuilder.ts
index 5b98b40..86a3130 100644
--- a/js/typescript-library/src/queryBuilder.ts
+++ b/js/typescript-library/src/queryBuilder.ts
@@ -75,7 +75,7 @@ class QueryBuilder implements HasCondition, HasHaving, HasJoin, HasStartedBuildi
 
   joinVirtual(virtualTable: VirtualTable, joinType: JoinType): HasStartedBuildingJoin {
     this.addJoinToQueryDto()
-    this.queryDto.virtualTable = virtualTable
+    this.queryDto.virtualTables.push(virtualTable)
     this.currentJoinTableBuilder = new JoinTableBuilder(this, virtualTable.name, joinType)
     return this.currentJoinTableBuilder
   }


Test patch:
diff --git a/core/src/test/java/io/squashql/query/ATestBucketing.java b/core/src/test/java/io/squashql/query/ATestBucketing.java
index 6050c86..d3240bc 100644
--- a/core/src/test/java/io/squashql/query/ATestBucketing.java
+++ b/core/src/test/java/io/squashql/query/ATestBucketing.java
@@ -2,28 +2,18 @@ package io.squashql.query;
 
 import io.squashql.TestClass;
 import io.squashql.query.builder.Query;
-import io.squashql.query.database.QueryRewriter;
-import io.squashql.query.database.SqlUtils;
-import io.squashql.query.dto.ConditionType;
-import io.squashql.query.dto.CriteriaDto;
-import io.squashql.query.dto.JoinType;
-import io.squashql.query.dto.VirtualTableDto;
+import io.squashql.query.dto.*;
 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 org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.function.BiFunction;
 
 import static io.squashql.query.Functions.*;
-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;
 
@@ -31,7 +21,38 @@ import static io.squashql.query.database.QueryEngine.TOTAL;
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public abstract class ATestBucketing extends ABaseTestQuery {
 
-  protected String storeName = "store" + getClass().getSimpleName().toLowerCase();
+  private final String storeName = "store" + getClass().getSimpleName().toLowerCase();
+  private final TableField ean = new TableField(this.storeName, "ean");
+  private final TableField shop = new TableField(this.storeName, "shop");
+  private final TableField unitPrice = new TableField(this.storeName, "unitPrice");
+  private final TableField qtySold = new TableField(this.storeName, "qtySold");
+  private final TableField kvi = new TableField(this.storeName, "kvi");
+
+  private final String sensitivitiesStoreName = "sensitivities";
+  private final TableField bucket = new TableField(this.sensitivitiesStoreName, "bucket");
+  private final TableField min = new TableField(this.sensitivitiesStoreName, "min");
+  private final TableField max = new TableField(this.sensitivitiesStoreName, "max");
+
+  private final String expensivenessStoreName = "expensiveness";
+  private final TableField category = new TableField(this.expensivenessStoreName, "category");
+  private final TableField minPrice = new TableField(this.expensivenessStoreName, "minPrice");
+  private final TableField maxPrice = new TableField(this.expensivenessStoreName, "maxPrice");
+  private final Measure sales = Functions.sum("sales", Functions.multiply(this.qtySold, this.unitPrice));
+  private final VirtualTableDto sensitivities = new VirtualTableDto(
+          this.sensitivitiesStoreName,
+          List.of(this.bucket.fieldName, this.min.fieldName, this.max.fieldName),
+          List.of(
+                  List.of("unsensitive", 0d, 50d),
+                  List.of("sensitive", 50d, 80d),
+                  List.of("hypersensitive", 80d, 101d)
+          ));
+  private final VirtualTableDto expensiveness = new VirtualTableDto(
+          this.expensivenessStoreName,
+          List.of(this.category.fieldName, this.minPrice.fieldName, this.maxPrice.fieldName),
+          List.of(
+                  List.of("cheap", 0d, 7d),
+                  List.of("expensive", 7d, 11d)
+          ));
 
   @Override
   protected Map<String, List<TableTypedField>> getFieldsByStore() {
@@ -54,34 +75,21 @@ public abstract class ATestBucketing extends ABaseTestQuery {
     this.tm.load(this.storeName, tuples);
   }
 
-  static VirtualTableDto sensitivities = new VirtualTableDto("sensitivities", List.of("bucket", "min", "max"), List.of(
-          List.of("unsensitive", 0d, 50d),
-          List.of("sensitive", 50d, 80d),
-          List.of("hypersensitive", 80d, 101d)
-  ));
-
-  @ParameterizedTest
-  @ValueSource(booleans = {true, false})
-  void test(boolean useFullName) {
-    QueryRewriter qr = this.executor.queryEngine.queryRewriter(null);
-    String expression = String.format("sum(%s * %s)", qr.fieldName("unitPrice"), qr.fieldName("qtySold"));
-    ExpressionMeasure sales = new ExpressionMeasure("sales", expression);
-    BiFunction<String, String, String> fieldNameGenerator = (table, field) -> useFullName ? SqlUtils.getFieldFullName(table, field) : field;
+  @Test
+  void testSingleVirtualTable() {
     CriteriaDto criteria = all(
-            criterion(fieldNameGenerator.apply(this.storeName, "kvi"), fieldNameGenerator.apply(sensitivities.name, "min"), ConditionType.GE),
-            criterion(fieldNameGenerator.apply(this.storeName, "kvi"), fieldNameGenerator.apply(sensitivities.name, "max"), ConditionType.LT));
-    Field bucket = tableField(fieldNameGenerator.apply(sensitivities.name, "bucket"));
-    Field shop = tableField(fieldNameGenerator.apply(this.storeName, "shop"));
+            criterion(this.kvi, this.min, ConditionType.GE),
+            criterion(this.kvi, this.max, ConditionType.LT));
     var query = Query
             .from(this.storeName)
-            .join(sensitivities, JoinType.INNER)
+            .join(this.sensitivities, JoinType.INNER)
             .on(criteria)
-            .select(List.of(shop, bucket), List.of(sales))
+            .select(List.of(this.shop, this.bucket), List.of(this.sales))
             .build();
 
     Table result = this.executor.executeQuery(query);
     Assertions.assertThat(result.headers().stream().map(Header::name))
-            .containsExactly(shop.name(), bucket.name(), "sales");
+            .containsExactly(this.shop.name(), this.bucket.name(), this.sales.alias());
     Assertions.assertThat(result).containsExactly(
             List.of("0", "hypersensitive", 240d),
             List.of("0", "sensitive", 150d),
@@ -92,14 +100,14 @@ public abstract class ATestBucketing extends ABaseTestQuery {
 
     query = Query
             .from(this.storeName)
-            .join(sensitivities, JoinType.INNER)
+            .join(this.sensitivities, JoinType.INNER)
             .on(criteria)
-            .select(List.of(shop, bucket), List.of(sales))
-            .rollup(shop, bucket)
+            .select(List.of(this.shop, this.bucket), List.of(this.sales))
+            .rollup(this.shop, this.bucket)
             .build();
     result = this.executor.executeQuery(query);
     Assertions.assertThat(result.headers().stream().map(Header::name))
-            .containsExactly(shop.name(), bucket.name(), "sales");
+            .containsExactly(this.shop.name(), this.bucket.name(), this.sales.alias());
     Assertions.assertThat(result).containsExactly(
             List.of(GRAND_TOTAL, GRAND_TOTAL, 900d),
             List.of("0", TOTAL, 450d),
@@ -114,27 +122,19 @@ public abstract class ATestBucketing extends ABaseTestQuery {
 
   @Test
   void testConditionFieldCombined() {
-    QueryRewriter qr = this.executor.queryEngine.queryRewriter(null);
-    String expression = String.format("sum(%s * %s)", qr.fieldName("unitPrice"), qr.fieldName("qtySold"));
-    ExpressionMeasure sales = new ExpressionMeasure("sales", expression);
-    TableField kvi = new TableField(this.storeName, "kvi");
-    TableField min = new TableField(sensitivities.name, "min");
-    TableField max = new TableField(sensitivities.name, "max");
     CriteriaDto criteria = all(
-            criterion(minus(kvi, min), ge(0)),
-            criterion(minus(kvi, max), lt(0)));
-    String bucket = SqlUtils.getFieldFullName(sensitivities.name, "bucket");
-    String shop = SqlUtils.getFieldFullName(this.storeName, "shop");
+            criterion(minus(this.kvi, this.min), ge(0)),
+            criterion(minus(this.kvi, this.max), lt(0)));
     var query = Query
             .from(this.storeName)
-            .join(sensitivities, JoinType.INNER)
+            .join(this.sensitivities, JoinType.INNER)
             .on(criteria)
-            .select(tableFields(List.of(shop, bucket)), List.of(sales))
+            .select(List.of(this.shop, this.bucket), List.of(this.sales))
             .build();
 
     Table result = this.executor.executeQuery(query);
     Assertions.assertThat(result.headers().stream().map(Header::name))
-            .containsExactly(shop, bucket, "sales");
+            .containsExactly(this.shop.name(), this.bucket.name(), this.sales.alias());
     Assertions.assertThat(result).containsExactly(
             List.of("0", "hypersensitive", 240d),
             List.of("0", "sensitive", 150d),
@@ -143,4 +143,30 @@ public abstract class ATestBucketing extends ABaseTestQuery {
             List.of("1", "sensitive", 150d),
             List.of("1", "unsensitive", 60d));
   }
+
+  @Test
+  void testMultipleVirtualTables() {
+    CriteriaDto criteriaSensi = all(
+            criterion(this.kvi, this.min, ConditionType.GE),
+            criterion(this.kvi, this.max, ConditionType.LT));
+    CriteriaDto criteriaExp = all(
+            criterion(this.unitPrice, this.minPrice, ConditionType.GE),
+            criterion(this.unitPrice, this.maxPrice, ConditionType.LT));
+    var query = Query
+            .from(this.storeName)
+            .join(this.sensitivities, JoinType.INNER)
+            .on(criteriaSensi)
+            .join(this.expensiveness, JoinType.INNER)
+            .on(criteriaExp)
+            .select(List.of(this.bucket, this.category), List.of(CountMeasure.INSTANCE))
+            .build();
+
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result.headers().stream().map(Header::name))
+            .containsExactly(this.bucket.name(), this.category.name(), CountMeasure.INSTANCE.alias());
+    Assertions.assertThat(result).containsExactly(
+            List.of("hypersensitive", "expensive", 6L),
+            List.of("sensitive", "cheap", 6L),
+            List.of("unsensitive", "cheap", 8L));
+  }
 }
diff --git a/core/src/test/java/io/squashql/query/TestSQLTranslator.java b/core/src/test/java/io/squashql/query/TestSQLTranslator.java
index 7289a92..b698ce8 100644
--- a/core/src/test/java/io/squashql/query/TestSQLTranslator.java
+++ b/core/src/test/java/io/squashql/query/TestSQLTranslator.java
@@ -372,18 +372,18 @@ public class TestSQLTranslator {
 
   @Test
   void testVirtualTable() {
-    final TableDto main = new TableDto(BASE_STORE_NAME);
-    final VirtualTableDto virtualTable = new VirtualTableDto(
+    TableDto main = new TableDto(BASE_STORE_NAME);
+    VirtualTableDto virtualTable = new VirtualTableDto(
             "virtual",
             List.of("a", "b"),
             List.of(List.of(0, "0"), List.of(1, "1")));
     main.join(new TableDto(virtualTable.name), INNER, criterion("id", "a", ConditionType.EQ));
-    final QueryDto query = new QueryDto()
+    QueryDto query = new QueryDto()
             .table(main)
             .withMeasure(sum("pnl.sum", "pnl"))
             .withColumn(tableField("id"))
             .withColumn(tableField("b"));
-    query.virtualTableDto = virtualTable;
+    query.virtualTableDtos = List.of(virtualTable);
     String expected = String.format("with %2$s as (select 0 as `a`, '0' as `b` union all select 1 as `a`, '1' as `b`) " +
                     "select `id`, `b`, sum(`pnl`) as `pnl.sum` from `%1$s` " +
                     "inner join %2$s on `id` = `a` group by `id`, `b`",
@@ -405,7 +405,7 @@ public class TestSQLTranslator {
             .withMeasure(sum("pnl.sum", "pnl"))
             .withColumn(tableField("id"))
             .withColumn(tableField("b"));
-    query.virtualTableDto = virtualTable;
+    query.virtualTableDtos = List.of(virtualTable);
 
     String expected = String.format("with %2$s as (select 0 as `a`, '0' as `b` union all select 1 as `a`, '1' as `b`) " +
                     "select `id`, `b`, sum(`pnl`) as `pnl.sum` from `%1$s` " +
diff --git a/core/src/test/java/io/squashql/query/builder/TestQuery.java b/core/src/test/java/io/squashql/query/builder/TestQuery.java
index 14cb0b9..3c581a0 100644
--- a/core/src/test/java/io/squashql/query/builder/TestQuery.java
+++ b/core/src/test/java/io/squashql/query/builder/TestQuery.java
@@ -201,7 +201,7 @@ public class TestQuery {
             .withColumn(tableField("col1"))
             .withColumn(tableField("col2"))
             .withMeasure(sum);
-    q.virtualTableDto = vt;
+    q.virtualTableDtos = List.of(vt);
 
     Assertions.assertThat(build).isEqualTo(q);
   }
diff --git a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
index a9d3ea8..7fb3237 100644
--- a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
+++ b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
@@ -119,7 +119,8 @@ public class TestJavascriptLibrary {
   void testReadJsonBuildFromQuery() {
     var table = new TableDto("myTable");
     var refTable = new TableDto("refTable");
-    var cte = new VirtualTableDto("myCte", List.of("id", "min", "max", "other"), List.of(List.of(0, 0, 1, "x"), List.of(1, 2, 3, "y")));
+    var cte1 = new VirtualTableDto("myCte1", List.of("id", "min", "max", "other"), List.of(List.of(0, 0, 1, "x"), List.of(1, 2, 3, "y")));
+    var cte2 = new VirtualTableDto("myCte2", List.of("id", "min", "max", "other"), List.of(List.of(0, 4, 12, "a"), List.of(1, 12, 25, "b")));
 
     BucketColumnSetDto bucketColumnSet = new BucketColumnSetDto("group", tableField("scenario"))
             .withNewBucket("a", List.of("a1", "a2"))
@@ -131,8 +132,10 @@ public class TestJavascriptLibrary {
             .join(refTable.name, JoinType.INNER)
             .on(all(criterion("myTable" + ".id", "refTable" + ".id", ConditionType.EQ),
                     criterion("myTable" + ".a", "refTable" + ".a", ConditionType.EQ)))
-            .join(cte, JoinType.INNER)
-            .on(all(criterion("myTable.value", "myCte.min", ConditionType.GE), criterion("myTable.value", "myCte.max", ConditionType.LT)))
+            .join(cte1, JoinType.INNER)
+            .on(all(criterion("myTable.value", "myCte1.min", ConditionType.GE), criterion("myTable.value", "myCte1.max", ConditionType.LT)))
+            .join(cte2, JoinType.INNER)
+            .on(all(criterion("myTable.value", "myCte2.min", ConditionType.GE), criterion("myTable.value", "myCte2.max", ConditionType.LT)))
             .where(tableField("f2"), gt(659))
             .where(tableField("f3"), eq(123))
             .select(tableFields(List.of("a", "b")),
@@ -158,7 +161,7 @@ public class TestJavascriptLibrary {
     Assertions.assertThat(q.table).isEqualTo(qjs.table);
     Assertions.assertThat(q.subQuery).isEqualTo(qjs.subQuery);
     Assertions.assertThat(q.limit).isEqualTo(qjs.limit);
-    Assertions.assertThat(q.virtualTableDto).isEqualTo(qjs.virtualTableDto);
+    Assertions.assertThat(q.virtualTableDtos).isEqualTo(qjs.virtualTableDtos);
     Assertions.assertThat(q).isEqualTo(qjs);
   }
 
diff --git a/server/src/test/resources/build-from-query-join.json b/server/src/test/resources/build-from-query-join.json
index e6ccd2f..2b7b67a 100644
--- a/server/src/test/resources/build-from-query-join.json
+++ b/server/src/test/resources/build-from-query-join.json
@@ -1,1 +1,1 @@
-{"first":{"table":{"name":"myTable1","joins":[]},"columns":[{"@class":"io.squashql.query.TableField","fullName":"myTable1.a","tableName":"myTable1","fieldName":"a"},{"@class":"io.squashql.query.TableField","fullName":"myTable1.b","tableName":"myTable1","fieldName":"b"},{"@class":"io.squashql.query.TableField","fullName":"myTable1.c","tableName":"myTable1","fieldName":"c"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f1","fieldName":"f1"},"aggregationFunction":"sum","alias":"sum"}],"orders":{},"limit":-1},"second":{"table":{"name":"myTable2","joins":[]},"columns":[{"@class":"io.squashql.query.TableField","fullName":"myTable2.b","tableName":"myTable2","fieldName":"b"},{"@class":"io.squashql.query.TableField","fullName":"myTable2.c","tableName":"myTable2","fieldName":"c"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f2","fieldName":"f2"},"aggregationFunction":"avg","alias":"sum"}],"orders":{},"limit":-1},"joinType":"LEFT","joinCondition":{"conditionType":"AND","children":[{"field":{"@class":"io.squashql.query.TableField","fullName":"myTable1.b","tableName":"myTable1","fieldName":"b"},"fieldOther":{"@class":"io.squashql.query.TableField","fullName":"myTable2.b","tableName":"myTable2","fieldName":"b"},"conditionType":"EQ"},{"field":{"@class":"io.squashql.query.TableField","fullName":"myTable1.c","tableName":"myTable1","fieldName":"c"},"fieldOther":{"@class":"io.squashql.query.TableField","fullName":"myTable2.c","tableName":"myTable2","fieldName":"c"},"conditionType":"EQ"}]},"orders":{"{\"@class\":\"io.squashql.query.TableField\",\"fullName\":\"myTable1.a\",\"tableName\":\"myTable1\",\"fieldName\":\"a\"}":{"@class":"io.squashql.query.dto.SimpleOrderDto","order":"ASC"}},"limit":10}
\ No newline at end of file
+{"first":{"table":{"name":"myTable1","joins":[]},"virtualTableDtos":[],"columns":[{"@class":"io.squashql.query.TableField","fullName":"myTable1.a","tableName":"myTable1","fieldName":"a"},{"@class":"io.squashql.query.TableField","fullName":"myTable1.b","tableName":"myTable1","fieldName":"b"},{"@class":"io.squashql.query.TableField","fullName":"myTable1.c","tableName":"myTable1","fieldName":"c"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f1","fieldName":"f1"},"aggregationFunction":"sum","alias":"sum"}],"orders":{},"limit":-1},"second":{"table":{"name":"myTable2","joins":[]},"virtualTableDtos":[],"columns":[{"@class":"io.squashql.query.TableField","fullName":"myTable2.b","tableName":"myTable2","fieldName":"b"},{"@class":"io.squashql.query.TableField","fullName":"myTable2.c","tableName":"myTable2","fieldName":"c"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f2","fieldName":"f2"},"aggregationFunction":"avg","alias":"sum"}],"orders":{},"limit":-1},"joinType":"LEFT","joinCondition":{"conditionType":"AND","children":[{"field":{"@class":"io.squashql.query.TableField","fullName":"myTable1.b","tableName":"myTable1","fieldName":"b"},"fieldOther":{"@class":"io.squashql.query.TableField","fullName":"myTable2.b","tableName":"myTable2","fieldName":"b"},"conditionType":"EQ"},{"field":{"@class":"io.squashql.query.TableField","fullName":"myTable1.c","tableName":"myTable1","fieldName":"c"},"fieldOther":{"@class":"io.squashql.query.TableField","fullName":"myTable2.c","tableName":"myTable2","fieldName":"c"},"conditionType":"EQ"}]},"orders":{"{\"@class\":\"io.squashql.query.TableField\",\"fullName\":\"myTable1.a\",\"tableName\":\"myTable1\",\"fieldName\":\"a\"}":{"@class":"io.squashql.query.dto.SimpleOrderDto","order":"ASC"}},"limit":10}
\ No newline at end of file
diff --git a/server/src/test/resources/build-from-query-merge-pivot.json b/server/src/test/resources/build-from-query-merge-pivot.json
index db506d4..df1d4e3 100644
--- a/server/src/test/resources/build-from-query-merge-pivot.json
+++ b/server/src/test/resources/build-from-query-merge-pivot.json
@@ -1,1 +1,1 @@
-{"query":{"first":{"table":{"name":"myTable","joins":[]},"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":"sum","alias":"sum"}],"orders":{},"limit":-1},"second":{"table":{"name":"myTable","joins":[]},"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},"joinType":"LEFT"},"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":{"first":{"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":"sum","alias":"sum"}],"orders":{},"limit":-1},"second":{"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},"joinType":"LEFT"},"rows":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"}],"columns":[{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}]}
\ No newline at end of file
diff --git a/server/src/test/resources/build-from-query-pivot.json b/server/src/test/resources/build-from-query-pivot.json
index 5880ef6..d1dcdcf 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":[]},"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"}]}
\ No newline at end of file