Repository: squashql/squashql
Pull request #281 (2024-03-05 10:30:40)

Single group comparison
See the documentation: https://github.com/squashql/squashql/blob/main/documentation/QUERY.md#group-comparison---columnset (end of **Group comparison - ColumnSet** section)


Patch:
diff --git a/core/src/main/java/io/squashql/query/QueryExecutor.java b/core/src/main/java/io/squashql/query/QueryExecutor.java
index 54469ec..61b5f9d 100644
--- a/core/src/main/java/io/squashql/query/QueryExecutor.java
+++ b/core/src/main/java/io/squashql/query/QueryExecutor.java
@@ -251,6 +251,14 @@ public class QueryExecutor {
             .hitCount(stats.hitCount)
             .evictionCount(stats.evictionCount)
             .missCount(stats.missCount);
+
+    if (query.columnSets.containsKey(GROUP)) {
+      GroupColumnSetDto columnSet = (GroupColumnSetDto) query.columnSets.get(GROUP);
+      if (columnSet.values.size() == 1) { // only one group, no need to keep the additional column
+        result.removeColumn(SqlUtils.squashqlExpression(columnSet.newField));
+      }
+    }
+
     return result;
   }
 
diff --git a/core/src/main/java/io/squashql/table/ColumnarTable.java b/core/src/main/java/io/squashql/table/ColumnarTable.java
index 921dbe6..10628bd 100644
--- a/core/src/main/java/io/squashql/table/ColumnarTable.java
+++ b/core/src/main/java/io/squashql/table/ColumnarTable.java
@@ -1,6 +1,5 @@
 package io.squashql.table;
 
-import com.google.common.base.Suppliers;
 import io.squashql.query.Header;
 import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.dictionary.ObjectArrayDictionary;
@@ -11,11 +10,10 @@ import java.util.function.Supplier;
 
 public class ColumnarTable implements Table {
 
-  protected final List<Header> headers;
-  protected final Set<CompiledMeasure> measures;
-
-  public final Supplier<ObjectArrayDictionary> pointDictionary;
-  protected final List<List<Object>> values;
+  private final MemoizingSupplier<ObjectArrayDictionary> pointDictionary;
+  private final List<Header> headers;
+  private final Set<CompiledMeasure> measures;
+  private final List<List<Object>> values;
 
   public ColumnarTable(List<Header> headers, Set<CompiledMeasure> measures, List<List<Object>> values) {
     if (headers.stream().filter(Header::isMeasure)
@@ -26,7 +24,7 @@ public class ColumnarTable implements Table {
     this.headers = new ArrayList<>(headers);
     this.measures = new HashSet<>(measures);
     this.values = new ArrayList<>(values);
-    this.pointDictionary = Suppliers.memoize(() -> createPointDictionary(this));
+    this.pointDictionary = new MemoizingSupplier<>(() -> createPointDictionary(this));
   }
 
   public static ObjectArrayDictionary createPointDictionary(Table table) {
@@ -56,6 +54,7 @@ public class ColumnarTable implements Table {
    * BE CAREFUL !! This method assumes this table and the table from passed in arguments have the same headers
    * {@code Header#isMeasure == false} in the same order.
    */
+  @Override
   public void transferAggregates(Table from, CompiledMeasure measure) {
     if (this.headers.stream().filter(h -> !h.isMeasure()).count() !=
             from.headers().stream().filter(h -> !h.isMeasure()).count()) {
@@ -84,6 +83,18 @@ public class ColumnarTable implements Table {
     }
   }
 
+  @Override
+  public void removeColumn(String column) {
+    int index = columnIndex(column);
+    Header header = this.headers.remove(index);
+    if (header.isMeasure()) {
+      this.measures.removeIf(m -> m.alias().equals(header.name()));
+    } else {
+      this.pointDictionary.forget();
+    }
+    this.values.remove(index);
+  }
+
   @Override
   public List<Object> getColumn(int columnIndex) {
     return this.values.get(columnIndex);
@@ -164,4 +175,38 @@ public class ColumnarTable implements Table {
   public int hashCode() {
     return Objects.hash(this.headers, this.measures, this.values);
   }
+
+  private static class MemoizingSupplier<T> implements Supplier<T> {
+
+    private final Supplier<T> delegate;
+    private boolean initialized;
+    private T value;
+
+    MemoizingSupplier(Supplier<T> delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public T get() {
+      if (!this.initialized) {
+        T t = this.delegate.get();
+        this.value = t;
+        this.initialized = true;
+        return t;
+      }
+      return this.value;
+    }
+
+    public void forget() {
+      this.value = null;
+      this.initialized = false;
+    }
+
+    @Override
+    public String toString() {
+      return "Suppliers.memoize("
+              + (this.initialized ? "<supplier that returned " + this.value + ">" : this.delegate)
+              + ")";
+    }
+  }
 }
diff --git a/core/src/main/java/io/squashql/table/PivotTableUtils.java b/core/src/main/java/io/squashql/table/PivotTableUtils.java
index 7553388..e730a6e 100644
--- a/core/src/main/java/io/squashql/table/PivotTableUtils.java
+++ b/core/src/main/java/io/squashql/table/PivotTableUtils.java
@@ -56,7 +56,7 @@ public final class PivotTableUtils {
         for (int j = 0; j < colIndices.length; j++) {
           buffer[colIndices[j]] = headerColumns.get(j).get(i * values.size());
         }
-        int position = ((ColumnarTable) table).pointDictionary.get().getPosition(buffer);
+        int position = table.pointDictionary().getPosition(buffer);
 
         for (String value : values) {
           r.add(position >= 0 ? table.getColumnValues(value).get(position) : null);
diff --git a/core/src/main/java/io/squashql/table/RowTable.java b/core/src/main/java/io/squashql/table/RowTable.java
index fba3e3a..f29c9ac 100644
--- a/core/src/main/java/io/squashql/table/RowTable.java
+++ b/core/src/main/java/io/squashql/table/RowTable.java
@@ -30,6 +30,11 @@ public class RowTable implements Table {
     throwNotSupportedException();
   }
 
+  @Override
+  public void removeColumn(String column) {
+    throwNotSupportedException();
+  }
+
   @Override
   public List<Object> getColumn(int columnIndex) {
     throwNotSupportedException();
diff --git a/core/src/main/java/io/squashql/table/Table.java b/core/src/main/java/io/squashql/table/Table.java
index 781229e..7c54b72 100644
--- a/core/src/main/java/io/squashql/table/Table.java
+++ b/core/src/main/java/io/squashql/table/Table.java
@@ -34,6 +34,8 @@ public interface Table extends Iterable<List<Object>> {
 
   void transferAggregates(Table from, CompiledMeasure measure);
 
+  void removeColumn(String column);
+
   default List<Object> getColumn(int columnIndex) {
     List<Object> elements = new ArrayList<>();
     for (List<Object> objects : this) {
diff --git a/core/src/main/java/io/squashql/table/TableUtils.java b/core/src/main/java/io/squashql/table/TableUtils.java
index 59637d4..17c92dc 100644
--- a/core/src/main/java/io/squashql/table/TableUtils.java
+++ b/core/src/main/java/io/squashql/table/TableUtils.java
@@ -31,14 +31,14 @@ public class TableUtils {
     return toString(null, rows, null, rowElementPrinters, predicate);
   }
 
-  public static String toString(List<? extends Object> columns,
+  public static String toString(List<?> columns,
                                 Iterable<List<Object>> rows,
                                 Function<Object, String> columnElementPrinters,
                                 Function<Object, String> rowElementPrinters) {
     return toString(columns, rows, columnElementPrinters, rowElementPrinters, __ -> false);
   }
 
-  public static String toString(List<? extends Object> columns,
+  public static String toString(List<?> columns,
                                 Iterable<List<Object>> rows,
                                 Function<Object, String> columnElementPrinters,
                                 Function<Object, String> rowElementPrinters,
@@ -201,7 +201,7 @@ public class TableUtils {
       copy.remove(SqlUtils.squashqlExpression(cs.field));
     });
 
-    List<Header> headers = table.headers;
+    List<Header> headers = table.headers();
     for (Header header : headers) {
       String headerName = header.name();
       Comparator<?> queryComp = comparatorByColumnName.get(headerName);
@@ -230,11 +230,11 @@ public class TableUtils {
     int[] finalIndices = MultipleColumnsSorter.sort(args, comparators, contextIndices);
 
     List<List<Object>> values = new ArrayList<>();
-    for (List<Object> value : table.values) {
+    for (List<Object> value : table.getColumns()) {
       values.add(reorder(value, finalIndices));
     }
 
-    return new ColumnarTable(headers, table.measures, values);
+    return new ColumnarTable(headers, table.measures(), values);
   }
 
   /**
@@ -253,11 +253,11 @@ public class TableUtils {
     boolean[] lazilyCreated = new boolean[1];
     Supplier<Table> finalTable = Suppliers.memoize(() -> {
       List<List<Object>> newValues = new ArrayList<>();
-      for (int i = 0; i < table.headers.size(); i++) {
+      for (int i = 0; i < table.headers().size(); i++) {
         newValues.add(new ArrayList<>(table.getColumn(i)));
       }
       lazilyCreated[0] = true;
-      return new ColumnarTable(table.headers, table.measures, newValues);
+      return new ColumnarTable(table.headers(), table.measures(), newValues);
     });
 
     for (int rowIndex = 0; rowIndex < table.count(); rowIndex++) {
diff --git a/documentation/QUERY.md b/documentation/QUERY.md
index 1364c32..11bde8b 100644
--- a/documentation/QUERY.md
+++ b/documentation/QUERY.md
@@ -1026,7 +1026,7 @@ The second argument is the name of the existing column whose values will be grou
 The third argument is the defined groups to be used for the comparison. The orders of the keys (group1, group2....)
 and in the arrays are important.
 
-We can use the `GroupColumnSet` as follows
+We can use the `GroupColumnSet` as follows, as the second argument of `select`
 ```typescript
 import {GroupColumnSet, ExpressionMeasure, from} from "@squashql/squashql-js"
 
@@ -1077,17 +1077,19 @@ const values = new Map(Object.entries({
 const saleprice = new TableField("myTable.saleprice")
 const loavessold = new TableField("myTable.loavessold")
 const revenue = sum("revenue", saleprice.multiply(loavessold))
-const columnSet = new GroupColumnSet(new TableField("group"), new TableField("myTable.scenario"), groups)
+const scenario = new TableField("myTable.scenario")
+const columnSet = new GroupColumnSet(new TableField("group"), scenario, groups)
 const revenueComparison = comparisonMeasureWithinSameGroup("revenueComparison",
         ComparisonMethod.ABSOLUTE_DIFFERENCE,
         revenue,
-        new Map([[new TableField("myTable.scenario"), "s-1"]]))
+        new Map([[scenario, "s-1"]]))
 const query = from("myTable")
         .select([], [columnSet], [revenue, revenueComparison])
         .build()
 ```
 
-`{"scenario": "s-1"}`, the "translation" or "shift" operator indicates that each value is to be compared with the one for the previous scenario (in the current group).
+`{scenario: "s-1"}`, the "translation" or "shift" operator indicates that each value is to be compared with the one for 
+the previous scenario (in the current group).
 This is why order in `values` is important. Usage of `first` keyword is also possible (see time-series comparison).
 
 Result
@@ -1108,6 +1110,25 @@ Result
 +--------+----------+---------+-------------------+
 ```
 
+Note: if only one group is specified, such as:
+```typescript
+const values = new Map(Object.entries({
+  "group4": ["s1", "s2", "s3"],
+}))
+```
+
+the additional column named "group" is not added to the final result.
+
+```
++----------+---------+-------------------+
+| scenario | revenue | revenueComparison |
++----------+---------+-------------------+
+|       s1 |   372.0 |              12.0 |
+|       s2 |   300.0 |             -72.0 |
+|       s3 |   350.0 |              50.0 |
++----------+---------+-------------------+
+```
+
 ## Parameters
 
 Once built, a query can accept additional parameters to change the way the query will be executed. Here's 
diff --git a/js/typescript-library/package-lock.json b/js/typescript-library/package-lock.json
index 5923b48..3f9333e 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.12.2",
+  "version": "1.13.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@squashql/squashql-js",
-      "version": "1.12.2",
+      "version": "1.13.0",
       "license": "Apache-2.0",
       "dependencies": {
         "axios": "^1.6.7"


Test patch:
diff --git a/core/src/test/java/io/squashql/query/ATestGroupComparison.java b/core/src/test/java/io/squashql/query/ATestGroupComparison.java
index e6a4fb2..10286b3 100644
--- a/core/src/test/java/io/squashql/query/ATestGroupComparison.java
+++ b/core/src/test/java/io/squashql/query/ATestGroupComparison.java
@@ -27,11 +27,15 @@ import static io.squashql.transaction.DataLoader.SCENARIO_FIELD_NAME;
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public abstract class ATestGroupComparison extends ABaseTestQuery {
 
-  protected String storeName = "store" + getClass().getSimpleName().toLowerCase();
-
-  protected String groupOfScenario = "Group of scenario";
-
-  protected GroupColumnSetDto groupCS = new GroupColumnSetDto(this.groupOfScenario, tableField(SCENARIO_FIELD_NAME))
+  private static final String groupOfScenario = "Group of scenario";
+  private final String storeName = "store" + getClass().getSimpleName().toLowerCase();
+  private final TableField ean = new TableField(this.storeName, "ean");
+  private final TableField category = new TableField(this.storeName, "category");
+  private final TableField price = new TableField(this.storeName, "price");
+  private final TableField qty = new TableField(this.storeName, "quantity");
+  private final TableField scenario = new TableField(this.storeName, SCENARIO_FIELD_NAME);
+
+  protected GroupColumnSetDto groupCS = new GroupColumnSetDto(groupOfScenario, tableField(SCENARIO_FIELD_NAME))
           .withNewGroup("group1", List.of(MAIN_SCENARIO_NAME, "s1"))
           .withNewGroup("group2", List.of(MAIN_SCENARIO_NAME, "s2"))
           .withNewGroup("group3", List.of(MAIN_SCENARIO_NAME, "s1", "s2"));
@@ -70,7 +74,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
   @ValueSource(booleans = {true, false})
   void testAbsoluteDifferenceWithFirst(boolean fullName) {
     Field scenario = fullName ? new TableField(this.storeName, SCENARIO_FIELD_NAME) : new TableField(SCENARIO_FIELD_NAME);
-    GroupColumnSetDto group = new GroupColumnSetDto(this.groupOfScenario, scenario)
+    GroupColumnSetDto group = new GroupColumnSetDto(groupOfScenario, scenario)
             .withNewGroup("group1", List.of(MAIN_SCENARIO_NAME, "s1"))
             .withNewGroup("group2", List.of(MAIN_SCENARIO_NAME, "s2"))
             .withNewGroup("group3", List.of(MAIN_SCENARIO_NAME, "s1", "s2"));
@@ -82,7 +86,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             price,
             Map.of(
                     scenario, AComparisonExecutor.REF_POS_FIRST,
-                    tableField(this.groupOfScenario), "g"
+                    tableField(groupOfScenario), "g"
             ),
             ColumnSetKey.GROUP);
     AggregatedMeasure quantity = new AggregatedMeasure("q", "quantity", "sum");
@@ -92,7 +96,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             quantity,
             Map.of(
                     scenario, AComparisonExecutor.REF_POS_FIRST,
-                    tableField(this.groupOfScenario), "g"
+                    tableField(groupOfScenario), "g"
             ),
             ColumnSetKey.GROUP);
 
@@ -103,7 +107,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
 
     Table dataset = this.executor.executeQuery(query);
     Assertions.assertThat(dataset.headers().stream().map(Header::name)).containsExactly(
-            this.groupOfScenario, fullName ? SqlUtils.getFieldFullName(this.storeName, SCENARIO_FIELD_NAME) : SCENARIO_FIELD_NAME,
+            groupOfScenario, fullName ? SqlUtils.getFieldFullName(this.storeName, SCENARIO_FIELD_NAME) : SCENARIO_FIELD_NAME,
             "priceDiff", "p",
             "quantityDiff", "q");
     Assertions.assertThat(dataset).containsExactlyInAnyOrder(
@@ -137,7 +141,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             price,
             Map.of(
                     tableField(SCENARIO_FIELD_NAME), "s-1",
-                    tableField(this.groupOfScenario), "g"
+                    tableField(groupOfScenario), "g"
             ),
             ColumnSetKey.GROUP);
     AggregatedMeasure quantity = new AggregatedMeasure("q", "quantity", "sum");
@@ -145,7 +149,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             "quantityDiff",
             ComparisonMethod.ABSOLUTE_DIFFERENCE,
             quantity,
-            Map.of(tableField(SCENARIO_FIELD_NAME), "s-1", tableField(this.groupOfScenario), "g"),
+            Map.of(tableField(SCENARIO_FIELD_NAME), "s-1", tableField(groupOfScenario), "g"),
             ColumnSetKey.GROUP);
 
     var query = new QueryDto()
@@ -158,7 +162,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
 
     Table dataset = this.executor.executeQuery(query);
     Assertions.assertThat(dataset.headers().stream().map(Header::name)).containsExactly(
-            this.groupOfScenario, SCENARIO_FIELD_NAME,
+            groupOfScenario, SCENARIO_FIELD_NAME,
             "priceDiff", "p",
             "quantityDiff", "q");
     Assertions.assertThat(dataset).containsExactlyInAnyOrder(
@@ -180,7 +184,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             price,
             Map.of(
                     tableField(SCENARIO_FIELD_NAME), AComparisonExecutor.REF_POS_FIRST,
-                    tableField(this.groupOfScenario), "g"
+                    tableField(groupOfScenario), "g"
             ),
             ColumnSetKey.GROUP);
     AggregatedMeasure quantity = new AggregatedMeasure("q", "quantity", "sum");
@@ -190,7 +194,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             quantity,
             Map.of(
                     tableField(SCENARIO_FIELD_NAME), AComparisonExecutor.REF_POS_FIRST,
-                    tableField(this.groupOfScenario), "g"
+                    tableField(groupOfScenario), "g"
             ),
             ColumnSetKey.GROUP);
 
@@ -204,7 +208,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
 
     Table dataset = this.executor.executeQuery(query);
     Assertions.assertThat(dataset.headers().stream().map(Header::name)).containsExactly(
-            this.groupOfScenario, SCENARIO_FIELD_NAME,
+            groupOfScenario, SCENARIO_FIELD_NAME,
             "priceDiff", "p",
             "quantityDiff", "q");
     Assertions.assertThat(dataset).containsExactlyInAnyOrder(
@@ -220,7 +224,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
   @Test
   void testOrderIsPreserved() {
     // The following order should be respected even if columns are ordered by default.
-    GroupColumnSetDto groupCS = new GroupColumnSetDto(this.groupOfScenario, tableField(SCENARIO_FIELD_NAME))
+    GroupColumnSetDto groupCS = new GroupColumnSetDto(groupOfScenario, tableField(SCENARIO_FIELD_NAME))
             .withNewGroup("B", List.of("s1", MAIN_SCENARIO_NAME))
             .withNewGroup("A", List.of("s2", MAIN_SCENARIO_NAME, "s1"))
             .withNewGroup("C", List.of(MAIN_SCENARIO_NAME, "s2", "s1"));
@@ -232,7 +236,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
 
     Table dataset = this.executor.executeQuery(query);
     Assertions.assertThat(dataset.headers().stream().map(Header::name))
-            .containsExactly(this.groupOfScenario, SCENARIO_FIELD_NAME, CountMeasure.ALIAS);
+            .containsExactly(groupOfScenario, SCENARIO_FIELD_NAME, CountMeasure.ALIAS);
     Assertions.assertThat(dataset).containsExactly(
             List.of("B", "s1", 3l),
             List.of("B", MAIN_SCENARIO_NAME, 3l),
@@ -247,7 +251,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
   @Test
   void testOrderIsPreservedAndNaturallyOrderOnOtherColumns() {
     // The following order should be respected even if columns are ordered by default.
-    GroupColumnSetDto groupCS = new GroupColumnSetDto(this.groupOfScenario, tableField(SCENARIO_FIELD_NAME))
+    GroupColumnSetDto groupCS = new GroupColumnSetDto(groupOfScenario, tableField(SCENARIO_FIELD_NAME))
             .withNewGroup("B", List.of("s1", MAIN_SCENARIO_NAME))
             .withNewGroup("A", List.of("s2", MAIN_SCENARIO_NAME));
 
@@ -278,7 +282,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
   @Test
   void testTotal() {
     // The following order should be respected even if columns are ordered by default.
-    GroupColumnSetDto groupCS = new GroupColumnSetDto(this.groupOfScenario, tableField(SCENARIO_FIELD_NAME))
+    GroupColumnSetDto groupCS = new GroupColumnSetDto(groupOfScenario, tableField(SCENARIO_FIELD_NAME))
             .withNewGroup("B", List.of("s1", MAIN_SCENARIO_NAME))
             .withNewGroup("A", List.of("s2", MAIN_SCENARIO_NAME, "s1"))
             .withNewGroup("C", List.of(MAIN_SCENARIO_NAME, "s2", "s1"));
@@ -291,7 +295,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
 
     Table dataset = this.executor.executeQuery(query);
     Assertions.assertThat(dataset.headers().stream().map(Header::name))
-            .containsExactly(this.groupOfScenario, SCENARIO_FIELD_NAME, CountMeasure.ALIAS);
+            .containsExactly(groupOfScenario, SCENARIO_FIELD_NAME, CountMeasure.ALIAS);
     Assertions.assertThat(dataset).containsExactly(
             List.of("B", "s1", 3l),
             List.of("B", MAIN_SCENARIO_NAME, 3l),
@@ -302,4 +306,33 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             List.of("C", "s2", 3l),
             List.of("C", "s1", 3l));
   }
+
+  @Test
+  void testAbsoluteDifferenceWithPreviousSingleGroup() {
+    GroupColumnSetDto group = new GroupColumnSetDto(groupOfScenario, this.scenario)
+            .withNewGroup("whatever", List.of(MAIN_SCENARIO_NAME, "s1", "s2")); // Name of the group is not important when single group
+
+    AggregatedMeasure price = new AggregatedMeasure("p", "price", "sum");
+    ComparisonMeasureReferencePosition priceComp = new ComparisonMeasureReferencePosition(
+            "priceDiff",
+            ComparisonMethod.ABSOLUTE_DIFFERENCE,
+            price,
+            Map.of(this.scenario, "s-1"),
+            ColumnSetKey.GROUP);
+
+    var query = Query
+            .from(this.storeName)
+            .select_(List.of(group), List.of(priceComp, price))
+            .build();
+
+    Table dataset = this.executor.executeQuery(query);
+    Assertions.assertThat(dataset.headers().stream().map(Header::name)).containsExactly(
+            SqlUtils.getFieldFullName(this.storeName, SCENARIO_FIELD_NAME),
+            "priceDiff",
+            "p");
+    Assertions.assertThat(dataset).containsExactlyInAnyOrder(
+            List.of("base", 0d, 15d),
+            List.of("s1", 2d, 17d),
+            List.of("s2", -2.5d, 14.5d));
+  }
 }
diff --git a/core/src/test/java/io/squashql/query/ATestVectorAggregation.java b/core/src/test/java/io/squashql/query/ATestVectorAggregation.java
index 459fe72..66e3802 100644
--- a/core/src/test/java/io/squashql/query/ATestVectorAggregation.java
+++ b/core/src/test/java/io/squashql/query/ATestVectorAggregation.java
@@ -422,7 +422,7 @@ public abstract class ATestVectorAggregation extends ABaseTestQuery {
   private void assertVectorValues(ColumnarTable result, Measure vectorMeasure, List<List<Object>> points, List<List<Number>> expectedVectors) {
     List<Object> aggregateValues = result.getColumnValues(vectorMeasure.alias());
     for (int i = 0; i < points.size(); i++) {
-      ObjectArrayDictionary dictionary = result.pointDictionary.get();
+      ObjectArrayDictionary dictionary = result.pointDictionary();
       int position = dictionary.getPosition(points.get(i).toArray());
       Object actual = aggregateValues.get(position);
       // SORT to have a deterministic comparison
@@ -435,7 +435,7 @@ public abstract class ATestVectorAggregation extends ABaseTestQuery {
   private void assertValues(ColumnarTable result, Measure otherMeasure, List<List<Object>> points, List<Number> expectedValues) {
     List<Object> aggregateValues = result.getColumnValues(otherMeasure.alias());
     for (int i = 0; i < points.size(); i++) {
-      ObjectArrayDictionary dictionary = result.pointDictionary.get();
+      ObjectArrayDictionary dictionary = result.pointDictionary();
       int position = dictionary.getPosition(points.get(i).toArray());
       Object actual = aggregateValues.get(position);
       Assertions.assertThat(actual).isEqualTo(expectedValues.get(i));
diff --git a/core/src/test/java/io/squashql/query/ATestVectorAggregationDataType.java b/core/src/test/java/io/squashql/query/ATestVectorAggregationDataType.java
index 311e864..40fa069 100644
--- a/core/src/test/java/io/squashql/query/ATestVectorAggregationDataType.java
+++ b/core/src/test/java/io/squashql/query/ATestVectorAggregationDataType.java
@@ -89,7 +89,7 @@ public abstract class ATestVectorAggregationDataType extends ABaseTestQuery {
     Assertions.assertThat(header.type()).isEqualTo(expectedType);
     List<Object> aggregateValues = result.getColumnValues(vectorMeasure.alias());
     for (int i = 0; i < points.size(); i++) {
-      ObjectArrayDictionary dictionary = result.pointDictionary.get();
+      ObjectArrayDictionary dictionary = result.pointDictionary();
       int position = dictionary.getPosition(points.get(i).toArray());
       List<?> actual = (List<?>) aggregateValues.get(position);
       // SORT to have a deterministic comparison
diff --git a/core/src/test/java/io/squashql/table/TestTable.java b/core/src/test/java/io/squashql/table/TestTable.java
index 21e74b7..a6c596c 100644
--- a/core/src/test/java/io/squashql/table/TestTable.java
+++ b/core/src/test/java/io/squashql/table/TestTable.java
@@ -75,4 +75,33 @@ public class TestTable {
             .isInstanceOf(IllegalArgumentException.class)
             .hasMessageContaining("does not match the headers of the destination table");
   }
+
+  @Test
+  void testRemoveColumn() {
+    Header pop = new Header("population.avg", double.class, true);
+    Header city = new Header("city", String.class, false);
+    Header country = new Header("country", String.class, false);
+    CompiledAggregatedMeasure popAvg = new CompiledAggregatedMeasure("population.avg", new AliasedTypedField("population"), AVG, null, false);
+    ColumnarTable table = new ColumnarTable(
+            List.of(country, city, pop),
+            Set.of(popAvg),
+            List.of(
+                    Arrays.asList("france", "france", "spain", "spain"),
+                    Arrays.asList("paris", "toulouse", "madrid", "barcelona"),
+                    Arrays.asList(1d, 2d, 3d, 4d)));
+    Assertions.assertThat(table.headers().stream().map(Header::name)).containsExactly(country.name(), city.name(), pop.name());
+    Assertions.assertThat(table.measures()).containsExactly(popAvg);
+    Assertions.assertThat(table.pointDictionary().getPointLength()).isEqualTo(2);
+
+    table.removeColumn(country.name());
+    // Make sure the point dictionary is rebuilt
+    Assertions.assertThat(table.headers().stream().map(Header::name)).containsExactly(city.name(), pop.name());
+    Assertions.assertThat(table.measures()).containsExactly(popAvg);
+    Assertions.assertThat(table.pointDictionary().getPointLength()).isEqualTo(1);
+
+    table.removeColumn(pop.name());
+    Assertions.assertThat(table.headers().stream().map(Header::name)).containsExactly(city.name());
+    Assertions.assertThat(table.measures()).isEmpty();
+    Assertions.assertThat(table.pointDictionary().getPointLength()).isEqualTo(1);
+  }
 }