diff --git a/benchmark/pom.xml b/benchmark/pom.xml
index 88ee01a56..5b552f99c 100644
--- a/benchmark/pom.xml
+++ b/benchmark/pom.xml
@@ -125,7 +125,7 @@
         </plugin>
         <plugin>
           <artifactId>maven-install-plugin</artifactId>
-          <version>3.1.3</version>
+          <version>3.1.4</version>
         </plugin>
         <plugin>
           <artifactId>maven-jar-plugin</artifactId>
diff --git a/extensions/database/pom.xml b/extensions/database/pom.xml
index ed9948802..25302ef82 100644
--- a/extensions/database/pom.xml
+++ b/extensions/database/pom.xml
@@ -84,7 +84,7 @@
     <dependency>
       <groupId>com.google.http-client</groupId>
       <artifactId>google-http-client-jackson2</artifactId>
-      <version>1.46.2</version>
+      <version>1.46.3</version>
       <exclusions>
         <exclusion>
           <groupId>org.apache.httpcomponents</groupId>
diff --git a/main/src/com/google/refine/operations/cell/KeyValueColumnizeOperation.java b/main/src/com/google/refine/operations/cell/KeyValueColumnizeOperation.java
index ea78ff24d..1fde3c6c4 100644
--- a/main/src/com/google/refine/operations/cell/KeyValueColumnizeOperation.java
+++ b/main/src/com/google/refine/operations/cell/KeyValueColumnizeOperation.java
@@ -116,6 +116,14 @@ public class KeyValueColumnizeOperation extends AbstractOperation {
         return Optional.empty();
     }

+    @Override
+    public KeyValueColumnizeOperation renameColumns(Map<String, String> newColumnNames) {
+        return new KeyValueColumnizeOperation(
+                newColumnNames.getOrDefault(_keyColumnName, _keyColumnName),
+                newColumnNames.getOrDefault(_valueColumnName, _valueColumnName),
+                _noteColumnName == null ? null : newColumnNames.getOrDefault(_noteColumnName, _noteColumnName));
+    }
+
     @Override
     protected HistoryEntry createHistoryEntry(Project project, long historyEntryID) throws Exception {
         int keyColumnIndex = project.columnModel.getColumnIndexByName(_keyColumnName);
diff --git a/main/src/com/google/refine/operations/cell/MultiValuedCellJoinOperation.java b/main/src/com/google/refine/operations/cell/MultiValuedCellJoinOperation.java
index f5397aeed..9730538f4 100644
--- a/main/src/com/google/refine/operations/cell/MultiValuedCellJoinOperation.java
+++ b/main/src/com/google/refine/operations/cell/MultiValuedCellJoinOperation.java
@@ -35,6 +35,7 @@ package com.google.refine.operations.cell;

 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;

@@ -101,6 +102,14 @@ public class MultiValuedCellJoinOperation extends AbstractOperation {
         return Optional.of(ColumnsDiff.modifySingleColumn(_columnName));
     }

+    @Override
+    public MultiValuedCellJoinOperation renameColumns(Map<String, String> newColumnNames) {
+        return new MultiValuedCellJoinOperation(
+                newColumnNames.getOrDefault(_columnName, _columnName),
+                newColumnNames.getOrDefault(_keyColumnName, _keyColumnName),
+                _separator);
+    }
+
     @Override
     protected String getBriefDescription(Project project) {
         return OperationDescription.cell_multivalued_cell_join_brief(_columnName);
diff --git a/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java b/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java
index 4dbecdca2..cfba9de77 100644
--- a/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java
+++ b/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java
@@ -35,6 +35,7 @@ package com.google.refine.operations.cell;

 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -188,6 +189,22 @@ public class MultiValuedCellSplitOperation extends AbstractOperation {
         return Optional.of(ColumnsDiff.modifySingleColumn(_columnName));
     }

+    @Override
+    public MultiValuedCellSplitOperation renameColumns(Map<String, String> newColumnNames) {
+        if ("separator".equals(_mode)) {
+            return new MultiValuedCellSplitOperation(
+                    newColumnNames.getOrDefault(_columnName, _columnName),
+                    newColumnNames.getOrDefault(_keyColumnName, _keyColumnName),
+                    _separator,
+                    _regex);
+        } else {
+            return new MultiValuedCellSplitOperation(
+                    newColumnNames.getOrDefault(_columnName, _columnName),
+                    newColumnNames.getOrDefault(_keyColumnName, _keyColumnName),
+                    _fieldLengths);
+        }
+    }
+
     @Override
     protected HistoryEntry createHistoryEntry(Project project, long historyEntryID) throws Exception {
         Column column = project.columnModel.getColumnByName(_columnName);
diff --git a/main/src/com/google/refine/operations/cell/TextTransformOperation.java b/main/src/com/google/refine/operations/cell/TextTransformOperation.java
index 27d823f4a..95fb78364 100644
--- a/main/src/com/google/refine/operations/cell/TextTransformOperation.java
+++ b/main/src/com/google/refine/operations/cell/TextTransformOperation.java
@@ -35,6 +35,7 @@ package com.google.refine.operations.cell;

 import java.io.Serializable;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
@@ -142,6 +143,25 @@ public class TextTransformOperation extends EngineDependentMassCellOperation {
         return Optional.of(ColumnsDiff.modifySingleColumn(_columnName));
     }

+    @Override
+    public TextTransformOperation renameColumns(Map<String, String> newColumnNames) {
+        String renamedExpression;
+        try {
+            Evaluable evaluable = MetaParser.parse(_expression);
+            Evaluable renamedEvaluable = evaluable.renameColumnDependencies(newColumnNames);
+            renamedExpression = renamedEvaluable.getFullSource();
+        } catch (ParsingException e) {
+            return this;
+        }
+        return new TextTransformOperation(
+                _engineConfig.renameColumnDependencies(newColumnNames),
+                newColumnNames.getOrDefault(_columnName, _columnName),
+                renamedExpression,
+                _onError,
+                _repeat,
+                _repeatCount);
+    }
+
     @Override
     protected RowVisitor createRowVisitor(Project project, List<CellChange> cellChanges, long historyEntryID) throws Exception {
         Column column = project.columnModel.getColumnByName(_columnName);
diff --git a/main/src/com/google/refine/operations/cell/TransposeColumnsIntoRowsOperation.java b/main/src/com/google/refine/operations/cell/TransposeColumnsIntoRowsOperation.java
index eb68a2cd8..9fec75744 100644
--- a/main/src/com/google/refine/operations/cell/TransposeColumnsIntoRowsOperation.java
+++ b/main/src/com/google/refine/operations/cell/TransposeColumnsIntoRowsOperation.java
@@ -35,6 +35,7 @@ package com.google.refine.operations.cell;

 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;

@@ -203,6 +204,28 @@ public class TransposeColumnsIntoRowsOperation extends AbstractOperation {
         return Optional.empty();
     }

+    @Override
+    public TransposeColumnsIntoRowsOperation renameColumns(Map<String, String> newColumnNames) {
+        if (_combinedColumnName != null) {
+            return new TransposeColumnsIntoRowsOperation(
+                    newColumnNames.getOrDefault(_startColumnName, _startColumnName),
+                    _columnCount,
+                    _ignoreBlankCells,
+                    _fillDown,
+                    newColumnNames.getOrDefault(_combinedColumnName, _combinedColumnName),
+                    _prependColumnName,
+                    _separator);
+        } else {
+            return new TransposeColumnsIntoRowsOperation(
+                    newColumnNames.getOrDefault(_startColumnName, _startColumnName),
+                    _columnCount,
+                    _ignoreBlankCells,
+                    _fillDown,
+                    newColumnNames.getOrDefault(_keyColumnName, _keyColumnName),
+                    newColumnNames.getOrDefault(_valueColumnName, _valueColumnName));
+        }
+    }
+
     @Override
     protected HistoryEntry createHistoryEntry(Project project, long historyEntryID) throws Exception {
         if (_combinedColumnName != null) {
diff --git a/main/src/com/google/refine/operations/cell/TransposeRowsIntoColumnsOperation.java b/main/src/com/google/refine/operations/cell/TransposeRowsIntoColumnsOperation.java
index d087d34f5..3201a4958 100644
--- a/main/src/com/google/refine/operations/cell/TransposeRowsIntoColumnsOperation.java
+++ b/main/src/com/google/refine/operations/cell/TransposeRowsIntoColumnsOperation.java
@@ -35,6 +35,7 @@ package com.google.refine.operations.cell;

 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;

@@ -95,6 +96,13 @@ public class TransposeRowsIntoColumnsOperation extends AbstractOperation {
         return Optional.empty();
     }

+    @Override
+    public TransposeRowsIntoColumnsOperation renameColumns(Map<String, String> newColumnNames) {
+        return new TransposeRowsIntoColumnsOperation(
+                newColumnNames.getOrDefault(_columnName, _columnName),
+                _rowCount);
+    }
+
     @Override
     protected HistoryEntry createHistoryEntry(Project project, long historyEntryID) throws Exception {
         List<Column> newColumns = new ArrayList<Column>();
diff --git a/main/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperation.java b/main/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperation.java
index 0d44f7aa5..1ed0a9e06 100644
--- a/main/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperation.java
+++ b/main/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperation.java
@@ -39,6 +39,7 @@ import java.io.IOException;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
@@ -227,6 +228,28 @@ public class ColumnAdditionByFetchingURLsOperation extends EngineDependentOperat
         return Optional.of(ColumnsDiff.builder().addColumn(_newColumnName, _baseColumnName).build());
     }

+    @Override
+    public ColumnAdditionByFetchingURLsOperation renameColumns(Map<String, String> newColumnNames) {
+        String renamedExpression;
+        try {
+            Evaluable evaluable = MetaParser.parse(_urlExpression);
+            Evaluable renamedEvaluable = evaluable.renameColumnDependencies(newColumnNames);
+            renamedExpression = renamedEvaluable.getFullSource();
+        } catch (ParsingException e) {
+            return this;
+        }
+        return new ColumnAdditionByFetchingURLsOperation(
+                _engineConfig.renameColumnDependencies(newColumnNames),
+                newColumnNames.getOrDefault(_baseColumnName, _baseColumnName),
+                renamedExpression,
+                _onError,
+                newColumnNames.getOrDefault(_newColumnName, _newColumnName),
+                _columnInsertIndex,
+                _delay,
+                _cacheResponses,
+                _httpHeadersJson);
+    }
+
     @Override
     public Process createProcess(Project project, Properties options) throws Exception {
         Engine engine = createEngine(project);
diff --git a/main/src/com/google/refine/operations/column/ColumnMoveOperation.java b/main/src/com/google/refine/operations/column/ColumnMoveOperation.java
index 77e43e915..0ba909930 100644
--- a/main/src/com/google/refine/operations/column/ColumnMoveOperation.java
+++ b/main/src/com/google/refine/operations/column/ColumnMoveOperation.java
@@ -33,6 +33,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 package com.google.refine.operations.column;

+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;

@@ -105,4 +106,9 @@ public class ColumnMoveOperation extends AbstractOperation {
     public Optional<ColumnsDiff> getColumnsDiff() {
         return Optional.of(ColumnsDiff.empty());
     }
+
+    @Override
+    public ColumnMoveOperation renameColumns(Map<String, String> newColumnNames) {
+        return new ColumnMoveOperation(newColumnNames.getOrDefault(_columnName, _columnName), _index);
+    }
 }
diff --git a/main/src/com/google/refine/operations/column/ColumnRemovalOperation.java b/main/src/com/google/refine/operations/column/ColumnRemovalOperation.java
index eb20212aa..d1dcfd793 100644
--- a/main/src/com/google/refine/operations/column/ColumnRemovalOperation.java
+++ b/main/src/com/google/refine/operations/column/ColumnRemovalOperation.java
@@ -33,6 +33,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 package com.google.refine.operations.column;

+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;

@@ -84,6 +85,11 @@ public class ColumnRemovalOperation extends AbstractOperation {
         return Optional.of(ColumnsDiff.builder().deleteColumn(_columnName).build());
     }

+    @Override
+    public ColumnRemovalOperation renameColumns(Map<String, String> newColumnNames) {
+        return new ColumnRemovalOperation(newColumnNames.getOrDefault(_columnName, _columnName));
+    }
+
     @Override
     protected HistoryEntry createHistoryEntry(Project project, long historyEntryID) throws Exception {
         Column column = project.columnModel.getColumnByName(_columnName);
diff --git a/main/src/com/google/refine/operations/column/ColumnRenameOperation.java b/main/src/com/google/refine/operations/column/ColumnRenameOperation.java
index 69cc04d1a..f80112228 100644
--- a/main/src/com/google/refine/operations/column/ColumnRenameOperation.java
+++ b/main/src/com/google/refine/operations/column/ColumnRenameOperation.java
@@ -33,6 +33,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 package com.google.refine.operations.column;

+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;

@@ -92,6 +93,13 @@ public class ColumnRenameOperation extends AbstractOperation {
         return Optional.of(ColumnsDiff.builder().deleteColumn(_oldColumnName).addColumn(_newColumnName, _oldColumnName).build());
     }

+    @Override
+    public ColumnRenameOperation renameColumns(Map<String, String> newColumnNames) {
+        return new ColumnRenameOperation(
+                newColumnNames.getOrDefault(_oldColumnName, _oldColumnName),
+                newColumnNames.getOrDefault(_newColumnName, _newColumnName));
+    }
+
     @Override
     protected HistoryEntry createHistoryEntry(Project project, long historyEntryID) throws Exception {
         if (project.columnModel.getColumnByName(_oldColumnName) == null) {
diff --git a/main/src/com/google/refine/operations/column/ColumnReorderOperation.java b/main/src/com/google/refine/operations/column/ColumnReorderOperation.java
index eff3efeef..33d151e4e 100644
--- a/main/src/com/google/refine/operations/column/ColumnReorderOperation.java
+++ b/main/src/com/google/refine/operations/column/ColumnReorderOperation.java
@@ -34,6 +34,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 package com.google.refine.operations.column;

 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -84,6 +85,12 @@ public class ColumnReorderOperation extends AbstractOperation {
         return Optional.empty(); // we don't know what columns there were before, so we can't diff them
     }

+    @Override
+    public ColumnReorderOperation renameColumns(Map<String, String> newColumnNames) {
+        return new ColumnReorderOperation(
+                _columnNames.stream().map(name -> newColumnNames.getOrDefault(name, name)).collect(Collectors.toList()));
+    }
+
     @Override
     protected HistoryEntry createHistoryEntry(Project project, long historyEntryID) throws Exception {
         return new HistoryEntry(
diff --git a/main/src/com/google/refine/operations/column/ColumnSplitOperation.java b/main/src/com/google/refine/operations/column/ColumnSplitOperation.java
index 4bfb3b7c0..fa7406ccb 100644
--- a/main/src/com/google/refine/operations/column/ColumnSplitOperation.java
+++ b/main/src/com/google/refine/operations/column/ColumnSplitOperation.java
@@ -36,6 +36,7 @@ package com.google.refine.operations.column;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -217,6 +218,27 @@ public class ColumnSplitOperation extends EngineDependentOperation {
         return Optional.empty(); // sadly the columns created depend on the data and the name of existing columns
     }

+    @Override
+    public ColumnSplitOperation renameColumns(Map<String, String> newColumnNames) {
+        if ("separator".equals(_mode)) {
+            return new ColumnSplitOperation(
+                    _engineConfig.renameColumnDependencies(newColumnNames),
+                    newColumnNames.getOrDefault(_columnName, _columnName),
+                    _guessCellType,
+                    _removeOriginalColumn,
+                    _separator,
+                    _regex,
+                    _maxColumns);
+        } else {
+            return new ColumnSplitOperation(
+                    _engineConfig.renameColumnDependencies(newColumnNames),
+                    newColumnNames.getOrDefault(_columnName, _columnName),
+                    _guessCellType,
+                    _removeOriginalColumn,
+                    _fieldLengths);
+        }
+    }
+
     @Override
     protected HistoryEntry createHistoryEntry(Project project, long historyEntryID) throws Exception {
         Engine engine = createEngine(project);
diff --git a/main/webapp/modules/core/scripts/index/parser-interfaces/wikitext-parser-ui.html b/main/webapp/modules/core/scripts/index/parser-interfaces/wikitext-parser-ui.html
index b0f543ec9..79a6c3b47 100644
--- a/main/webapp/modules/core/scripts/index/parser-interfaces/wikitext-parser-ui.html
+++ b/main/webapp/modules/core/scripts/index/parser-interfaces/wikitext-parser-ui.html
@@ -1,46 +1,46 @@

-
-
<input type="checkbox" bind="wikiCheckbox" id="$reconcileWiki" />
+
+
<input type="checkbox" bind="wikiCheckbox" id="$reconcileWiki" />
         <input bind="wikiUrlInput" spellcheck="false" type="text" class="lightweight" size="30" id="$wikiUrl" />

-
<input type="checkbox" bind="headerLinesCheckbox" id="$headers" />
+
<input type="checkbox" bind="headerLinesCheckbox" id="$headers" />

           <input bind="headerLinesInput" type="number" class="lightweight" size="7" value="1" />

-
<input type="checkbox" bind="limitCheckbox" id="$limit" />
+
<input type="checkbox" bind="limitCheckbox" id="$limit" />

           <input bind="limitInput" type="number" class="lightweight" size="7" value="0" />

-
<input type="checkbox" bind="guessCellValueTypesCheckbox" id="$guess" />
+
<input type="checkbox" bind="guessCellValueTypesCheckbox" id="$guess" />

-
<input type="checkbox" bind="blankSpanningCellsCheckbox" id="$blank-spanning-cells" />
+
<input type="checkbox" bind="blankSpanningCellsCheckbox" id="$blank-spanning-cells" />

-
<input type="checkbox" bind="storeBlankRowsCheckbox" id="$store-blank-rows" />
+
<input type="checkbox" bind="storeBlankRowsCheckbox" id="$store-blank-rows" />

-
<input type="checkbox" bind="storeBlankColumnsCheckbox" id="$store-blank-columns" />
+
<input type="checkbox" bind="storeBlankColumnsCheckbox" id="$store-blank-columns" />

-
<input type="checkbox" bind="includeRawTemplatesCheckbox" id="$include-raw-templates" />
+
<input type="checkbox" bind="includeRawTemplatesCheckbox" id="$include-raw-templates" />

-
<input type="checkbox" bind="parseReferencesCheckbox" id="$parse-references" />
+
<input type="checkbox" bind="parseReferencesCheckbox" id="$parse-references" />

-
<input type="checkbox" bind="storeBlankCellsAsNullsCheckbox" id="$store-blank-cells" />
+
<input type="checkbox" bind="storeBlankCellsAsNullsCheckbox" id="$store-blank-cells" />

-
<input type="checkbox" bind="includeFileSourcesCheckbox" id="$include-file-sources" />
+
<input type="checkbox" bind="includeFileSourcesCheckbox" id="$include-file-sources" />

-
<input type="checkbox" bind="includeArchiveFileCheckbox" id="$include-archive-file" />
+
<input type="checkbox" bind="includeArchiveFileCheckbox" id="$include-archive-file" />

-
+
         <button class="button" bind="previewButton"></button>

-
-          <input type="checkbox" bind="disableAutoPreviewCheckbox" id="$disable" />
+
+          <input type="checkbox" bind="disableAutoPreviewCheckbox" id="$disable" />

diff --git a/pom.xml b/pom.xml
index e0daf734e..dc88d31eb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,10 +77,10 @@
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

-    <jackson.version>2.18.2</jackson.version>
+    <jackson.version>2.18.3</jackson.version>
     <servlet-api.version>4.0.1</servlet-api.version>
     <butterfly.version>1.2.6</butterfly.version>
-    <slf4j.version>2.0.16</slf4j.version>
+    <slf4j.version>2.0.17</slf4j.version>
     <log4j.version>2.24.3</log4j.version>
     <jetty.version>10.0.16</jetty.version>
     <okhttp.version>4.12.0</okhttp.version>
@@ -113,7 +113,7 @@
     <httpmime.version>4.5.14</httpmime.version>
     <owasp-encoder.version>1.3.1</owasp-encoder.version>
     <jasypt.version>1.9.3</jasypt.version>
-    <mockito.version>5.15.2</mockito.version>
+    <mockito.version>5.16.0</mockito.version>

     <build-helper-maven-plugin.version>3.6.0</build-helper-maven-plugin.version>
@@ -126,7 +126,7 @@
     <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
     <maven-clean-plugin.version>3.4.1</maven-clean-plugin.version>
     <git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>
-    <maven-deploy-plugin.version>3.1.3</maven-deploy-plugin.version>
+    <maven-deploy-plugin.version>3.1.4</maven-deploy-plugin.version>
     <license-plugin.version>2.5.0</license-plugin.version>
     <org-jvnet-localizer-plugin.version>1.31</org-jvnet-localizer-plugin.version>
   </properties>

diff --git a/main/tests/cypress/package.json b/main/tests/cypress/package.json
index 0ce2986de..df4d291c1 100644
--- a/main/tests/cypress/package.json
+++ b/main/tests/cypress/package.json
@@ -16,14 +16,14 @@
         "lint": "prettier --check . && eslint ."
     },
     "dependencies": {
-        "cypress": "14.0.3",
+        "cypress": "14.1.0",
         "cypress-file-upload": "^5.0.8"
     },
     "devDependencies": {
         "eslint": "^9.21.0",
         "eslint-config-google": "^0.14.0",
-        "eslint-config-prettier": "^10.0.1",
+        "eslint-config-prettier": "^10.0.2",
         "eslint-plugin-cypress": "^4.1.0",
-        "prettier": "3.5.2"
+        "prettier": "3.5.3"
     }
 }
diff --git a/main/tests/cypress/yarn.lock b/main/tests/cypress/yarn.lock
index 924c79ed7..cd1d6ef17 100644
--- a/main/tests/cypress/yarn.lock
+++ b/main/tests/cypress/yarn.lock
@@ -364,10 +364,10 @@ check-more-types@^2.24.0:
   resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz"
   integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=

-ci-info@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.0.0.tgz#65466f8b280fc019b9f50a5388115d17a63a44f2"
-  integrity sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==
+ci-info@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.1.0.tgz#92319d2fa29d2620180ea5afed31f589bc98cf83"
+  integrity sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==

 clean-stack@^2.0.0:
   version "2.2.0"
@@ -461,10 +461,10 @@ cypress-file-upload@^5.0.8:
   resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
   integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==

-cypress@14.0.3:
-  version "14.0.3"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.0.3.tgz#7873d21f61d0559edb9c041316d450cf7ef9d5fa"
-  integrity sha512-yIdvobANw3kS+KF/t5vwjjPNufBA8ux7iQHaWxPTkUw2yCKI72m9mKM24eOwE84Wk4ALPsSvEcGbDrwgmhr4RA==
+cypress@14.1.0:
+  version "14.1.0"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.1.0.tgz#b2dbe7bbc529dc0c93ffd4e0e9fa59763afba0b8"
+  integrity sha512-pPPj8Uu9NwjaaiXAEcjYZZmgsq6v9Zs1Nw6a+zRF+ANgYSNhH4S32SjFRsvMcuOHR/8dp4GBJhBPqIPSs+TxaA==
   dependencies:
     "@cypress/request" "^3.0.7"
     "@cypress/xvfb" "^1.2.4"
@@ -477,7 +477,7 @@ cypress@14.0.3:
     cachedir "^2.3.0"
     chalk "^4.1.0"
     check-more-types "^2.24.0"
-    ci-info "^4.0.0"
+    ci-info "^4.1.0"
     cli-cursor "^3.1.0"
     cli-table3 "~0.6.1"
     commander "^6.2.1"
@@ -609,10 +609,10 @@ eslint-config-google@^0.14.0:
   resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz"
   integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==

-eslint-config-prettier@^10.0.1:
-  version "10.0.1"
-  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz#fbb03bfc8db0651df9ce4e8b7150d11c5fe3addf"
-  integrity sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==
+eslint-config-prettier@^10.0.2:
+  version "10.0.2"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.0.2.tgz#47444de8aa104ce82c2f91ad2a5e96b62c01e20d"
+  integrity sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==

 eslint-plugin-cypress@^4.1.0:
   version "4.1.0"
@@ -1325,10 +1325,10 @@ prelude-ls@^1.2.1:
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz"
   integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==

-prettier@3.5.2:
-  version "3.5.2"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.2.tgz#d066c6053200da0234bf8fa1ef45168abed8b914"
-  integrity sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==
+prettier@3.5.3:
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5"
+  integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==

 pretty-bytes@^5.6.0:
   version "5.6.0"
diff --git a/main/tests/server/src/com/google/refine/operations/cell/KeyValueColumnizeTests.java b/main/tests/server/src/com/google/refine/operations/cell/KeyValueColumnizeTests.java
index a0fd572af..f5bb2140d 100644
--- a/main/tests/server/src/com/google/refine/operations/cell/KeyValueColumnizeTests.java
+++ b/main/tests/server/src/com/google/refine/operations/cell/KeyValueColumnizeTests.java
@@ -39,6 +39,7 @@ import static org.testng.Assert.assertThrows;
 import java.io.File;
 import java.io.IOException;
 import java.io.Serializable;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;

@@ -140,6 +141,38 @@ public class KeyValueColumnizeTests extends RefineTest {
         assertEquals(new KeyValueColumnizeOperation("foo", "baz", null).getColumnDependencies().get(), Set.of("foo", "baz"));
     }

+    @Test
+    public void testRename() {
+        KeyValueColumnizeOperation SUT = new KeyValueColumnizeOperation("foo", "baz", null);
+
+        KeyValueColumnizeOperation renamed = SUT.renameColumns(Map.of("foo", "foo2", "bar", "bar2"));
+
+        TestUtils.isSerializedTo(renamed, "{\n"
+                + "   \"description\" : " + new TextNode(OperationDescription.cell_key_value_columnize_brief("foo2", "baz")).toString()
+                + ",\n"
+                + "   \"keyColumnName\" : \"foo2\",\n"
+                + "   \"noteColumnName\" : null,\n"
+                + "   \"op\" : \"core/key-value-columnize\",\n"
+                + "   \"valueColumnName\" : \"baz\"\n"
+                + "}");
+    }
+
+    @Test
+    public void testRenameNoNotesColumn() {
+        KeyValueColumnizeOperation SUT = new KeyValueColumnizeOperation("foo", "baz", "bar");
+
+        KeyValueColumnizeOperation renamed = SUT.renameColumns(Map.of("foo", "foo2", "bar", "bar2"));
+
+        TestUtils.isSerializedTo(renamed, "{\n"
+                + "   \"description\" : "
+                + new TextNode(OperationDescription.cell_key_value_columnize_note_column_brief("foo2", "baz", "bar2")).toString() + ",\n"
+                + "   \"keyColumnName\" : \"foo2\",\n"
+                + "   \"noteColumnName\" : \"bar2\",\n"
+                + "   \"op\" : \"core/key-value-columnize\",\n"
+                + "   \"valueColumnName\" : \"baz\"\n"
+                + "}");
+    }
+
     /**
      * Test in the case where an ID is available in the first column.
      *
diff --git a/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellJoinOperationTests.java b/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellJoinOperationTests.java
index f5f047184..8a080f030 100644
--- a/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellJoinOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellJoinOperationTests.java
@@ -37,6 +37,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertThrows;

 import java.io.Serializable;
+import java.util.Map;
 import java.util.Set;

 import com.fasterxml.jackson.databind.node.TextNode;
@@ -120,6 +121,20 @@ public class MultiValuedCellJoinOperationTests extends RefineTest {
         assertEquals(new MultiValuedCellJoinOperation("value", "key", "sep").getColumnDependencies().get(), Set.of("value", "key"));
     }

+    @Test
+    public void testRename() {
+        var SUT = new MultiValuedCellJoinOperation("value", "key", "sep");
+
+        MultiValuedCellJoinOperation renamed = SUT.renameColumns(Map.of("value", "value2", "key", "key2", "sep", "sep2"));
+
+        String expectedJson = "{\"op\":\"core/multivalued-cell-join\","
+                + "\"description\":" + new TextNode(OperationDescription.cell_multivalued_cell_join_brief("value2")).toString() + ","
+                + "\"columnName\":\"value2\","
+                + "\"keyColumnName\":\"key2\","
+                + "\"separator\":\"sep\"}";
+        TestUtils.isSerializedTo(renamed, expectedJson);
+    }
+
     /*
      * Test to demonstrate the intended behaviour of the function
      */
diff --git a/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellSplitOperationTests.java b/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellSplitOperationTests.java
index 4ac3fa1fe..ed1adeb1a 100644
--- a/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellSplitOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellSplitOperationTests.java
@@ -37,6 +37,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertThrows;

 import java.io.Serializable;
+import java.util.Map;
 import java.util.Set;

 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -184,6 +185,28 @@ public class MultiValuedCellSplitOperationTests extends RefineTest {
                 false).getColumnDependencies().get(), Set.of("Value", "Key"));
     }

+    @Test
+    public void testRename() {
+        var SUT = new MultiValuedCellSplitOperation(
+                "Value",
+                "Key",
+                ":",
+                false);
+
+        MultiValuedCellSplitOperation renamed = SUT.renameColumns(Map.of("Value", "value2", "key", "Key2"));
+
+        String expectedJson = "{\n"
+                + "  \"columnName\" : \"value2\",\n"
+                + "  \"description\" : " + new TextNode(OperationDescription.cell_multivalued_cell_split_brief("value2")).toString() + ",\n"
+                + "  \"keyColumnName\" : \"Key\",\n"
+                + "  \"mode\" : \"separator\",\n"
+                + "  \"op\" : \"core/multivalued-cell-split\",\n"
+                + "  \"regex\" : false,\n"
+                + "  \"separator\" : \":\"\n"
+                + "}";
+        TestUtils.isSerializedTo(renamed, expectedJson);
+    }
+
     /**
      * Test to demonstrate the intended behaviour of the function, for issue #1268
      * https://github.com/OpenRefine/OpenRefine/issues/1268
diff --git a/main/tests/server/src/com/google/refine/operations/cell/TextTransformOperationTests.java b/main/tests/server/src/com/google/refine/operations/cell/TextTransformOperationTests.java
index 27aea691c..ccf0bcba0 100644
--- a/main/tests/server/src/com/google/refine/operations/cell/TextTransformOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/cell/TextTransformOperationTests.java
@@ -5,6 +5,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertThrows;

 import java.io.Serializable;
+import java.util.Map;
 import java.util.Set;

 import com.fasterxml.jackson.databind.node.TextNode;
@@ -117,6 +118,31 @@ public class TextTransformOperationTests extends RefineTest {
                 false, 0).getColumnDependencies().get(), Set.of("foo", "bar", "facet_1"));
     }

+    @Test
+    public void testRename() {
+        var SUT = new TextTransformOperation(
+                engineConfigWithColumnDeps,
+                "bar",
+                "grel:cells[\"foo\"].value+'_'+value",
+                OnError.SetToBlank,
+                false, 0);
+
+        TextTransformOperation renamed = SUT.renameColumns(Map.of("foo", "foo2", "bar", "bar2"));
+        String description = OperationDescription.cell_text_transform_brief("bar2",
+                "grel:cells.get(\"foo2\").value + '_' + value");
+        String expectedJson = "{\n"
+                + "  \"columnName\" : \"bar2\",\n"
+                + "  \"description\" : " + new TextNode(description).toString() + ",\n"
+                + "  \"engineConfig\" : null,\n"
+                + "  \"expression\" : \"grel:cells.get(\\\"foo2\\\").value + '_' + value\",\n"
+                + "  \"onError\" : \"set-to-blank\",\n"
+                + "  \"op\" : \"core/text-transform\",\n"
+                + "  \"repeat\" : false,\n"
+                + "  \"repeatCount\" : 0\n"
+                + "}";
+        TestUtils.isSerializedTo(renamed, expectedJson);
+    }
+
     @Test
     public void testTransformColumnInRowsMode() throws Exception {
         TextTransformOperation operation = new TextTransformOperation(
diff --git a/main/tests/server/src/com/google/refine/operations/cell/TransposeColumnsIntoRowsOperationTest.java b/main/tests/server/src/com/google/refine/operations/cell/TransposeColumnsIntoRowsOperationTest.java
index f26b458d4..5c66ea29a 100644
--- a/main/tests/server/src/com/google/refine/operations/cell/TransposeColumnsIntoRowsOperationTest.java
+++ b/main/tests/server/src/com/google/refine/operations/cell/TransposeColumnsIntoRowsOperationTest.java
@@ -5,6 +5,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertThrows;

 import java.io.Serializable;
+import java.util.Map;
 import java.util.Optional;

 import com.fasterxml.jackson.databind.node.TextNode;
@@ -66,6 +67,50 @@ public class TransposeColumnsIntoRowsOperationTest extends RefineTest {
                 Optional.empty());
     }

+    @Test
+    public void testRename() {
+        var SUT = new TransposeColumnsIntoRowsOperation("num1", -1, true, false, "a", true, ":");
+
+        TransposeColumnsIntoRowsOperation renamed = SUT.renameColumns(Map.of("num1", "num2", "a", "a2"));
+
+        TestUtils.isSerializedTo(renamed, "{\n"
+                + "  \"columnCount\" : -1,\n"
+                + "  \"combinedColumnName\" : \"a2\",\n"
+                + "  \"description\" : "
+                + new TextNode(OperationDescription.cell_transpose_columns_into_rows_combined_neg_brief("num2", "a2")).toString() + ",\n"
+                + "  \"fillDown\" : false,\n"
+                + "  \"ignoreBlankCells\" : true,\n"
+                + "  \"keyColumnName\" : null,\n"
+                + "  \"op\" : \"core/transpose-columns-into-rows\",\n"
+                + "  \"prependColumnName\" : true,\n"
+                + "  \"separator\" : \":\",\n"
+                + "  \"startColumnName\" : \"num2\",\n"
+                + "  \"valueColumnName\" : null\n"
+                + "}");
+    }
+
+    @Test
+    public void testRename2() {
+        var SUT = new TransposeColumnsIntoRowsOperation(
+                "b 1", 2, true, false, "key", "value");
+
+        TransposeColumnsIntoRowsOperation renamed = SUT.renameColumns(Map.of("b 1", "b", "key", "key2"));
+
+        TestUtils.isSerializedTo(renamed, "{\n"
+                + "  \"columnCount\" : 2,\n"
+                + "  \"description\" : " + new TextNode(OperationDescription.cell_transpose_columns_into_rows_not_combined_pos_brief(2, "b",
+                        "key2", "value")).toString()
+                + ",\n"
+                + "  \"fillDown\" : false,\n"
+                + "  \"ignoreBlankCells\" : true,\n"
+                + "  \"keyColumnName\" : \"key2\",\n"
+                + "  \"op\" : \"core/transpose-columns-into-rows\",\n"
+                + "  \"separator\" : null,\n"
+                + "  \"startColumnName\" : \"b\",\n"
+                + "  \"valueColumnName\" : \"value\"\n"
+                + "}");
+    }
+
     @Test
     public void testCreateHistoryEntry_transposeIntoOneColumn_removeRowForNullOrEmptyCell() throws Exception {
         Project project = createProject(
diff --git a/main/tests/server/src/com/google/refine/operations/cell/TransposeRowsIntoColumnsOperationTests.java b/main/tests/server/src/com/google/refine/operations/cell/TransposeRowsIntoColumnsOperationTests.java
index 767ec9e3c..ff00739cf 100644
--- a/main/tests/server/src/com/google/refine/operations/cell/TransposeRowsIntoColumnsOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/cell/TransposeRowsIntoColumnsOperationTests.java
@@ -37,6 +37,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertThrows;

 import java.io.Serializable;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;

@@ -101,6 +102,21 @@ public class TransposeRowsIntoColumnsOperationTests extends RefineTest {
         assertEquals(new TransposeRowsIntoColumnsOperation("b", 2).getColumnDependencies(), Optional.of(Set.of("b")));
     }

+    @Test
+    public void testRename() {
+        var SUT = new TransposeRowsIntoColumnsOperation("b", 2);
+
+        TransposeRowsIntoColumnsOperation renamed = SUT.renameColumns(Map.of("b", "c"));
+
+        TestUtils.isSerializedTo(renamed, "{\n"
+                + "  \"columnName\" : \"c\",\n"
+                + "  \"description\" : " + new TextNode(OperationDescription.cell_transpose_rows_into_columns_brief(2, "c")).toString()
+                + ",\n"
+                + "  \"op\" : \"core/transpose-rows-into-columns\",\n"
+                + "  \"rowCount\" : 2\n"
+                + "}");
+    }
+
     @Test
     public void testTransposeRowsIntoColumns() throws Exception {
         TransposeRowsIntoColumnsOperation operation = new TransposeRowsIntoColumnsOperation("b", 2);
diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperationTests.java
index e02b55b44..c4020c791 100644
--- a/main/tests/server/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperationTests.java
@@ -40,6 +40,7 @@ import static org.testng.Assert.assertTrue;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Random;
 import java.util.Set;
@@ -193,6 +194,25 @@ public class ColumnAdditionByFetchingURLsOperationTests extends RefineTest {
         assertThrows(IllegalArgumentException.class, () -> missingExpression.validate());
     }

+    @Test
+    public void testRename() {
+        ColumnAdditionByFetchingURLsOperation SUT = new ColumnAdditionByFetchingURLsOperation(engine_config,
+                "fruits",
+                "grel:\"https://example.com/api?city=\"+value",
+                OnError.StoreError,
+                "results",
+                1,
+                5,
+                true,
+                null);
+
+        ColumnAdditionByFetchingURLsOperation renamed = SUT.renameColumns(Map.of("fruits", "vegetables", "results", "json"));
+
+        assertEquals(renamed._baseColumnName, "vegetables");
+        assertEquals(renamed._newColumnName, "json");
+        assertEquals(renamed._urlExpression, "grel:\"https://example.com/api?city=\" + value");
+    }
+
     /**
      * Test for caching
      */
diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnMoveOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnMoveOperationTests.java
index 6d22be750..93127e85f 100644
--- a/main/tests/server/src/com/google/refine/operations/column/ColumnMoveOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnMoveOperationTests.java
@@ -31,6 +31,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertThrows;

 import java.io.Serializable;
+import java.util.Map;
 import java.util.Set;

 import com.fasterxml.jackson.databind.node.TextNode;
@@ -149,4 +150,13 @@ public class ColumnMoveOperationTests extends RefineTest {
         assertProjectEquals(project, expected);
     }

+    @Test
+    public void testRename() {
+        ColumnMoveOperation SUT = new ColumnMoveOperation("hello", 1);
+
+        ColumnMoveOperation renamed = SUT.renameColumns(Map.of("hello", "world"));
+
+        assertEquals(renamed._columnName, "world");
+    }
+
 }
diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnRemovalOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnRemovalOperationTests.java
index 8e9cba6c3..be9fa7721 100644
--- a/main/tests/server/src/com/google/refine/operations/column/ColumnRemovalOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnRemovalOperationTests.java
@@ -31,6 +31,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertThrows;

 import java.io.Serializable;
+import java.util.Map;
 import java.util.Set;

 import com.fasterxml.jackson.databind.node.TextNode;
@@ -103,4 +104,13 @@ public class ColumnRemovalOperationTests extends RefineTest {
                 });
         assertProjectEquals(project, expected);
     }
+
+    @Test
+    public void testRename() {
+        ColumnRemovalOperation SUT = new ColumnRemovalOperation("foo");
+
+        ColumnRemovalOperation renamed = SUT.renameColumns(Map.of("foo", "bar"));
+
+        assertEquals(renamed._columnName, "bar");
+    }
 }
diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnRenameOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnRenameOperationTests.java
index 0b0d73acc..f81175128 100644
--- a/main/tests/server/src/com/google/refine/operations/column/ColumnRenameOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnRenameOperationTests.java
@@ -31,6 +31,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertThrows;

 import java.io.Serializable;
+import java.util.Map;
 import java.util.Set;

 import com.fasterxml.jackson.databind.node.TextNode;
@@ -108,4 +109,14 @@ public class ColumnRenameOperationTests extends RefineTest {
                 });
         assertProjectEquals(project, expected);
     }
+
+    @Test
+    public void testRenameDependencies() throws Exception {
+        ColumnRenameOperation SUT = new ColumnRenameOperation("foo", "newfoo");
+
+        ColumnRenameOperation renamed = SUT.renameColumns(Map.of("foo", "foo2", "newfoo", "newfoo2"));
+
+        assertEquals(renamed._oldColumnName, "foo2");
+        assertEquals(renamed._newColumnName, "newfoo2");
+    }
 }
diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnReorderOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnReorderOperationTests.java
index ad4752669..c5b1480d7 100644
--- a/main/tests/server/src/com/google/refine/operations/column/ColumnReorderOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnReorderOperationTests.java
@@ -32,6 +32,8 @@ import static org.testng.Assert.assertThrows;

 import java.io.Serializable;
 import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;

@@ -125,4 +127,13 @@ public class ColumnReorderOperationTests extends RefineTest {
                 });
         assertProjectEquals(project, expected);
     }
+
+    @Test
+    public void testRename() {
+        ColumnReorderOperation SUT = new ColumnReorderOperation(Arrays.asList("c", "b"));
+
+        ColumnReorderOperation renamed = SUT.renameColumns(Map.of("a", "a2", "b", "b2"));
+
+        assertEquals(renamed._columnNames, List.of("c", "b2"));
+    }
 }
diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnSplitOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnSplitOperationTests.java
index b2cc93592..125049c09 100644
--- a/main/tests/server/src/com/google/refine/operations/column/ColumnSplitOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnSplitOperationTests.java
@@ -32,6 +32,7 @@ import static org.testng.Assert.assertThrows;

 import java.io.Serializable;
 import java.util.Collections;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;

@@ -258,4 +259,48 @@ public class ColumnSplitOperationTests extends RefineTest {
                 });
         assertProjectEquals(project, expected);
     }
+
+    @Test
+    public void testRenameLengths() {
+        ColumnSplitOperation SUT = new ColumnSplitOperation(EngineConfig.defaultRowBased(), "hello", false,
+                false, new int[] { 1, 2 });
+
+        ColumnSplitOperation renamed = SUT.renameColumns(Map.of("hello", "world"));
+        TestUtils.isSerializedTo(renamed, "{\n"
+                + "       \"columnName\" : \"world\",\n"
+                + "       \"description\" : " + new TextNode(OperationDescription.column_split_brief("world")).toString() + ",\n"
+                + "       \"engineConfig\" : {\n"
+                + "         \"facets\" : [ ],\n"
+                + "         \"mode\" : \"row-based\"\n"
+                + "       },\n"
+                + "       \"fieldLengths\" : [ 1, 2 ],\n"
+                + "       \"guessCellType\" : false,\n"
+                + "       \"mode\" : \"lengths\",\n"
+                + "       \"op\" : \"core/column-split\",\n"
+                + "       \"removeOriginalColumn\" : false\n"
+                + "     }");
+    }
+
+    @Test
+    public void testRenameSeparator() {
+        ColumnSplitOperation SUT = new ColumnSplitOperation(EngineConfig.defaultRowBased(), "foo", true, true, ",",
+                false, 2);
+
+        ColumnSplitOperation renamed = SUT.renameColumns(Map.of("foo", "bar"));
+        TestUtils.isSerializedTo(renamed, "{\n"
+                + "       \"columnName\" : \"bar\",\n"
+                + "       \"description\" : " + new TextNode(OperationDescription.column_split_separator_brief("bar")).toString() + ",\n"
+                + "       \"engineConfig\" : {\n"
+                + "         \"facets\" : [ ],\n"
+                + "         \"mode\" : \"row-based\"\n"
+                + "       },\n"
+                + "       \"guessCellType\" : true,\n"
+                + "       \"maxColumns\": 2,\n"
+                + "       \"regex\": false,\n"
+                + "       \"mode\" : \"separator\",\n"
+                + "       \"separator\": \",\",\n"
+                + "       \"op\" : \"core/column-split\",\n"
+                + "       \"removeOriginalColumn\" : true\n"
+                + "     }");
+    }
 }

Fixes #6498

Changes proposed in this pull request:
- remove the `width` attribute in the wikidata importer settings HTML to move the labels closer to the check boxes

Although the issue mentions a preference for moving away from using `table` to format the settings, I decided to leave the existing HTML as it's more consistent with the other settings HTML snippets. I think it makes more sense to track the migration of these snippets to more modern styles in a follow-up task.

I've attached a screenshot of the new settings. Given that `Disable auto preview` is below the `Update preview` button, I felt it made sense to leave the indentation for the final checkbox as-is, but I'd be happy to align the button and checkbox with the rest of the settings if others feel that is more appropriate.

![Screenshot from 2025-02-11 10-54-41](https://github.com/user-attachments/assets/e7819ff3-14c0-4747-9435-30a40f6bf87d)

def parser(log: str) -> dict[str, str]:
    import re
    import xml.etree.ElementTree as ET

    results: dict[str, str] = {}

    # Helper: merge statuses with precedence fail > skip > pass
    def merge_status(name: str, new_status: str):
        if new_status not in ("pass", "fail", "skip"):
            return
        old_status = results.get(name)
        if old_status is None:
            results[name] = new_status
            return
        if old_status == new_status:
            return
        # Precedence: fail > skip > pass
        priority = {"fail": 3, "skip": 2, "pass": 1}
        if priority[new_status] > priority[old_status]:
            results[name] = new_status

    # -----------------------
    # XML-based parsing pass
    # -----------------------

    # Non-greedy patterns for JUnit/TestNG XML blocks
    xml_pattern = re.compile(
        r"(<testng-results[\s\S]*?</testng-results>|<testsuite[\s\S]*?</testsuite>)",
        re.IGNORECASE,
    )

    for match in xml_pattern.finditer(log):
        xml_block = match.group(1)
        try:
            root = ET.fromstring(xml_block)
        except ET.ParseError:
            # Skip this block but continue with others
            continue

        tag_lower = root.tag.lower()

        # Handle TestNG summary reports (<testng-results>)
        if tag_lower.endswith("testng-results"):
            # Look for all test-method elements anywhere
            for tm in root.findall(".//test-method"):
                # Skip configuration methods
                if tm.attrib.get("is-config", "false").lower() == "true":
                    continue
                name = tm.attrib.get("name", "")
                if not name:
                    continue
                # Try to get class name from surrounding <class> element
                class_elem = tm.find("..")
                class_name = ""
                # xml.etree doesn't support parent lookup directly, so search ancestors:
                # We instead search for the nearest <class> ancestor manually:
            # Since parent lookup is tricky, fall back to the attribute 'class' on <test-method> if present
            # or to a surrounding <class> element found via xpath with name match, which is usually enough.
            # Re-parse with a simpler loop using suites/tests/classes for better class name resolution.
            for suite in root.findall(".//suite"):
                for test in suite.findall(".//test"):
                    for clazz in test.findall(".//class"):
                        class_name = clazz.attrib.get("name", "")
                        for tm in clazz.findall("test-method"):
                            if tm.attrib.get("is-config", "false").lower() == "true":
                                continue
                            m_name = tm.attrib.get("name", "")
                            if not m_name:
                                continue
                            fq_name = f"{class_name}.{m_name}" if class_name else m_name
                            status_attr = tm.attrib.get("status", "").upper()
                            # Map TestNG status to our statuses
                            if status_attr == "PASS":
                                status = "pass"
                            elif status_attr in ("FAIL", "FAILURE"):
                                status = "fail"
                            elif status_attr == "SKIP":
                                status = "skip"
                            else:
                                # Fallback: if there is an exception element, mark as fail; else pass
                                if tm.find(".//exception") is not None:
                                    status = "fail"
                                else:
                                    status = "pass"
                            merge_status(fq_name, status)
            continue

        # Handle JUnit-style <testsuite> reports
        if tag_lower.endswith("testsuite"):
            for tc in root.findall(".//testcase"):
                class_name = tc.attrib.get("classname", "")
                name = tc.attrib.get("name", "")
                if not name:
                    continue
                fq_name = f"{class_name}.{name}" if class_name else name

                # Determine status via children
                status = "pass"
                if tc.find("skipped") is not None:
                    status = "skip"
                elif tc.find("failure") is not None or tc.find("error") is not None:
                    status = "fail"

                merge_status(fq_name, status)

    # ---------------------------
    # Text-based parsing fallback
    # ---------------------------

    # Compile regexes for common frameworks

    # pytest: path::test_name STATUS
    pytest_re = re.compile(
        r"^(?P<name>\S+::\S+)\s+"
        r"(?P<status>PASSED|FAILED|SKIPPED|ERROR|XFAILED|XPASSED)\b"
    )

    # unittest / nose: ClassName.test_method ... ok/FAIL/ERROR/skipped
    unittest_re = re.compile(
        r"^(?P<class>[A-Za-z_]\w*)\.(?P<method>[A-Za-z_]\w*)\s+\.\.\.\s+"
        r"(?P<status>ok|FAIL|ERROR|skipped)\b"
    )

    # go test: --- STATUS: TestName
    gotest_re = re.compile(
        r"^---\s+(?P<status>PASS|FAIL|SKIP):\s+(?P<name>\S+)"
    )

    # Jest-like symbol lines: ✓/✕/○ test name
    jest_symbol_re = re.compile(r"^\s*([✓✕○])\s+(?P<name>.+)")

    # Map various raw statuses to our canonical ones
    def map_status(raw: str) -> str | None:
        r = raw.upper()
        if r in ("PASSED", "OK", "XPASSED", "PASS"):
            return "pass"
        if r in ("FAILED", "FAIL", "ERROR", "XFAILED"):
            return "fail"
        if r in ("SKIPPED", "SKIP", "SKIPPED (EXPECTED FAILURE)"):
            return "skip"
        return None

    for line in log.splitlines():
        line = line.rstrip("\n")

        # pytest
        m = pytest_re.match(line)
        if m:
            name = m.group("name")
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # unittest
        m = unittest_re.match(line)
        if m:
            name = f"{m.group('class')}.{m.group('method')}"
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # go test
        m = gotest_re.match(line)
        if m:
            name = m.group("name")
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # Jest symbols
        m = jest_symbol_re.match(line)
        if m:
            symbol = m.group(1)
            name = m.group("name").strip()
            if symbol == "✓":
                status = "pass"
            elif symbol == "✕":
                status = "fail"
            elif symbol == "○":
                status = "skip"
            else:
                status = None
            if status:
                merge_status(name, status)
            continue

    return results

export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq nodejs npm
./refine build

find . -maxdepth 6 -path '*surefire-reports/*.xml' -print0 | xargs -0 -I {} sh -c 'echo "===== {} ====="; cat {}'


diff --git a/main/resources/com/google/refine/operations/OperationDescription.properties b/main/resources/com/google/refine/operations/OperationDescription.properties
index 182ae9efc..e1e39b3ea 100644
--- a/main/resources/com/google/refine/operations/OperationDescription.properties
+++ b/main/resources/com/google/refine/operations/OperationDescription.properties
@@ -18,6 +18,10 @@ cell_transpose_rows_into_columns_brief=Transpose every {0} cells in column {1} i
 column_addition_by_fetching_urls_brief=Create column {0} at index {1} by fetching URLs based on column {2} using expression {3}
 column_addition_brief=Create column {0} at index {1} based on column {2} using expression {3}
 column_move_brief=Move column {0} to position {1}
+column_move_left_brief=Move column {0} to the left
+column_move_right_brief=Move column {0} to the right
+column_move_first_brief=Move column {0} to the beginning
+column_move_last_brief=Move column {0} to the end
 column_removal_brief=Remove column {0}
 column_rename_brief=Rename column {0} to {1}
 column_reorder_brief=Reorder columns
diff --git a/main/src/com/google/refine/operations/cell/MultiValuedCellJoinOperation.java b/main/src/com/google/refine/operations/cell/MultiValuedCellJoinOperation.java
index 9730538f4..ed65d5b4b 100644
--- a/main/src/com/google/refine/operations/cell/MultiValuedCellJoinOperation.java
+++ b/main/src/com/google/refine/operations/cell/MultiValuedCellJoinOperation.java
@@ -94,7 +94,7 @@ public class MultiValuedCellJoinOperation extends AbstractOperation {

     @Override
     public Optional<Set<String>> getColumnDependencies() {
-        return Optional.of(Set.of(_columnName, _keyColumnName));
+        return Optional.of(_columnName.equals(_keyColumnName) ? Set.of(_columnName) : Set.of(_columnName, _keyColumnName));
     }

     @Override
diff --git a/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java b/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java
index cfba9de77..d1d9b6ed1 100644
--- a/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java
+++ b/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java
@@ -181,7 +181,7 @@ public class MultiValuedCellSplitOperation extends AbstractOperation {

     @Override
     public Optional<Set<String>> getColumnDependencies() {
-        return Optional.of(Set.of(_columnName, _keyColumnName));
+        return Optional.of(_columnName.equals(_keyColumnName) ? Set.of(_columnName) : Set.of(_columnName, _keyColumnName));
     }

     @Override
diff --git a/main/src/com/google/refine/operations/column/AbstractColumnMoveOperation.java b/main/src/com/google/refine/operations/column/AbstractColumnMoveOperation.java
new file mode 100644
index 000000000..368f49c0d
--- /dev/null
+++ b/main/src/com/google/refine/operations/column/AbstractColumnMoveOperation.java
@@ -0,0 +1,63 @@
+
+package com.google.refine.operations.column;
+
+import java.util.Optional;
+import java.util.Set;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang.Validate;
+
+import com.google.refine.history.Change;
+import com.google.refine.history.HistoryEntry;
+import com.google.refine.model.AbstractOperation;
+import com.google.refine.model.ColumnsDiff;
+import com.google.refine.model.Project;
+import com.google.refine.model.changes.ColumnMoveChange;
+
+/**
+ * Base class for operations moving a single column to a new position.
+ */
+abstract class AbstractColumnMoveOperation extends AbstractOperation {
+
+    final protected String _columnName;
+
+    public AbstractColumnMoveOperation(
+            String columnName) {
+        _columnName = columnName;
+    }
+
+    @Override
+    public void validate() {
+        Validate.notNull(_columnName, "Missing column name");
+    }
+
+    @JsonProperty("columnName")
+    public String getColumnName() {
+        return _columnName;
+    }
+
+    protected abstract int getNewColumnIndex(int currentIndex, Project project);
+
+    @Override
+    protected HistoryEntry createHistoryEntry(Project project, long historyEntryID) throws Exception {
+        int index = project.columnModel.getColumnIndexByName(_columnName);
+        if (index == -1) {
+            throw new Exception("No column named " + _columnName);
+        }
+        int newIndex = getNewColumnIndex(index, project);
+
+        Change change = new ColumnMoveChange(_columnName, newIndex);
+
+        return new HistoryEntry(historyEntryID, project, getBriefDescription(null), this, change);
+    }
+
+    @Override
+    public Optional<Set<String>> getColumnDependencies() {
+        return Optional.of(Set.of(_columnName));
+    }
+
+    @Override
+    public Optional<ColumnsDiff> getColumnsDiff() {
+        return Optional.of(ColumnsDiff.empty());
+    }
+}
diff --git a/main/src/com/google/refine/operations/column/ColumnMoveFirstOperation.java b/main/src/com/google/refine/operations/column/ColumnMoveFirstOperation.java
new file mode 100644
index 000000000..b9ed53829
--- /dev/null
+++ b/main/src/com/google/refine/operations/column/ColumnMoveFirstOperation.java
@@ -0,0 +1,37 @@
+
+package com.google.refine.operations.column;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import com.google.refine.model.Project;
+import com.google.refine.operations.OperationDescription;
+
+/**
+ * Moves one column to the left-most position (first column of the project)
+ */
+public class ColumnMoveFirstOperation extends AbstractColumnMoveOperation {
+
+    @JsonCreator
+    public ColumnMoveFirstOperation(
+            @JsonProperty("columnName") String columnName) {
+        super(columnName);
+    }
+
+    @Override
+    protected String getBriefDescription(Project project) {
+        return OperationDescription.column_move_first_brief(_columnName);
+    }
+
+    @Override
+    protected int getNewColumnIndex(int currentIndex, Project project) {
+        return 0;
+    }
+
+    @Override
+    public ColumnMoveFirstOperation renameColumns(Map<String, String> renames) {
+        return new ColumnMoveFirstOperation(renames.getOrDefault(_columnName, _columnName));
+    }
+}
diff --git a/main/src/com/google/refine/operations/column/ColumnMoveLastOperation.java b/main/src/com/google/refine/operations/column/ColumnMoveLastOperation.java
new file mode 100644
index 000000000..1bb4bbf60
--- /dev/null
+++ b/main/src/com/google/refine/operations/column/ColumnMoveLastOperation.java
@@ -0,0 +1,37 @@
+
+package com.google.refine.operations.column;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import com.google.refine.model.Project;
+import com.google.refine.operations.OperationDescription;
+
+/**
+ * Moves a column to the end (last column of the project).
+ */
+public class ColumnMoveLastOperation extends AbstractColumnMoveOperation {
+
+    @JsonCreator
+    public ColumnMoveLastOperation(
+            @JsonProperty("columnName") String columnName) {
+        super(columnName);
+    }
+
+    @Override
+    protected String getBriefDescription(Project project) {
+        return OperationDescription.column_move_last_brief(_columnName);
+    }
+
+    @Override
+    protected int getNewColumnIndex(int currentIndex, Project project) {
+        return project.columnModel.columns.size() - 1;
+    }
+
+    @Override
+    public ColumnMoveLastOperation renameColumns(Map<String, String> renames) {
+        return new ColumnMoveLastOperation(renames.getOrDefault(_columnName, _columnName));
+    }
+}
diff --git a/main/src/com/google/refine/operations/column/ColumnMoveLeftOperation.java b/main/src/com/google/refine/operations/column/ColumnMoveLeftOperation.java
new file mode 100644
index 000000000..864c1ff82
--- /dev/null
+++ b/main/src/com/google/refine/operations/column/ColumnMoveLeftOperation.java
@@ -0,0 +1,37 @@
+
+package com.google.refine.operations.column;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import com.google.refine.model.Project;
+import com.google.refine.operations.OperationDescription;
+
+/**
+ * Moves one column to the left.
+ */
+public class ColumnMoveLeftOperation extends AbstractColumnMoveOperation {
+
+    @JsonCreator
+    public ColumnMoveLeftOperation(
+            @JsonProperty("columnName") String columnName) {
+        super(columnName);
+    }
+
+    @Override
+    protected String getBriefDescription(Project project) {
+        return OperationDescription.column_move_left_brief(_columnName);
+    }
+
+    @Override
+    protected int getNewColumnIndex(int currentIndex, Project project) {
+        return Math.max(0, currentIndex - 1);
+    }
+
+    @Override
+    public ColumnMoveLeftOperation renameColumns(Map<String, String> renames) {
+        return new ColumnMoveLeftOperation(renames.getOrDefault(_columnName, _columnName));
+    }
+}
diff --git a/main/src/com/google/refine/operations/column/ColumnMoveOperation.java b/main/src/com/google/refine/operations/column/ColumnMoveOperation.java
index 0ba909930..567cf59d4 100644
--- a/main/src/com/google/refine/operations/column/ColumnMoveOperation.java
+++ b/main/src/com/google/refine/operations/column/ColumnMoveOperation.java
@@ -34,77 +34,48 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 package com.google.refine.operations.column;

 import java.util.Map;
-import java.util.Optional;
-import java.util.Set;

 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import org.apache.commons.lang.Validate;

-import com.google.refine.history.Change;
-import com.google.refine.history.HistoryEntry;
-import com.google.refine.model.AbstractOperation;
-import com.google.refine.model.ColumnsDiff;
 import com.google.refine.model.Project;
-import com.google.refine.model.changes.ColumnMoveChange;
 import com.google.refine.operations.OperationDescription;

-public class ColumnMoveOperation extends AbstractOperation {
+public class ColumnMoveOperation extends AbstractColumnMoveOperation {

-    final protected String _columnName;
     final protected int _index;

     @JsonCreator
     public ColumnMoveOperation(
             @JsonProperty("columnName") String columnName,
             @JsonProperty("index") int index) {
-        _columnName = columnName;
+        super(columnName);
         _index = index;
     }

     @Override
     public void validate() {
-        Validate.notNull(_columnName, "Missing column name");
+        super.validate();
         Validate.isTrue(_index >= 0, "Invalid column index");
     }

-    @JsonProperty("columnName")
-    public String getColumnName() {
-        return _columnName;
-    }
-
     @JsonProperty("index")
     public int getIndex() {
         return _index;
     }

     @Override
-    protected String getBriefDescription(Project project) {
-        return OperationDescription.column_move_brief(_columnName, _index);
-    }
-
-    @Override
-    protected HistoryEntry createHistoryEntry(Project project, long historyEntryID) throws Exception {
-        if (project.columnModel.getColumnByName(_columnName) == null) {
-            throw new Exception("No column named " + _columnName);
-        }
+    protected int getNewColumnIndex(int currentIndex, Project project) {
         if (_index < 0 || _index >= project.columnModel.columns.size()) {
-            throw new Exception("New column index out of range " + _index);
+            throw new IllegalArgumentException("New column index out of range " + _index);
         }
-
-        Change change = new ColumnMoveChange(_columnName, _index);
-
-        return new HistoryEntry(historyEntryID, project, getBriefDescription(null), ColumnMoveOperation.this, change);
-    }
-
-    @Override
-    public Optional<Set<String>> getColumnDependencies() {
-        return Optional.of(Set.of(_columnName));
+        return _index;
     }

     @Override
-    public Optional<ColumnsDiff> getColumnsDiff() {
-        return Optional.of(ColumnsDiff.empty());
+    protected String getBriefDescription(Project project) {
+        return OperationDescription.column_move_brief(_columnName, _index);
     }

     @Override
diff --git a/main/src/com/google/refine/operations/column/ColumnMoveRightOperation.java b/main/src/com/google/refine/operations/column/ColumnMoveRightOperation.java
new file mode 100644
index 000000000..ce58659f8
--- /dev/null
+++ b/main/src/com/google/refine/operations/column/ColumnMoveRightOperation.java
@@ -0,0 +1,37 @@
+
+package com.google.refine.operations.column;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import com.google.refine.model.Project;
+import com.google.refine.operations.OperationDescription;
+
+/**
+ * Moves one column to the right.
+ */
+public class ColumnMoveRightOperation extends AbstractColumnMoveOperation {
+
+    @JsonCreator
+    public ColumnMoveRightOperation(
+            @JsonProperty("columnName") String columnName) {
+        super(columnName);
+    }
+
+    @Override
+    protected String getBriefDescription(Project project) {
+        return OperationDescription.column_move_right_brief(_columnName);
+    }
+
+    @Override
+    protected int getNewColumnIndex(int currentIndex, Project project) {
+        return Math.min(project.columnModel.columns.size() - 1, currentIndex + 1);
+    }
+
+    @Override
+    public ColumnMoveRightOperation renameColumns(Map<String, String> renames) {
+        return new ColumnMoveRightOperation(renames.getOrDefault(_columnName, _columnName));
+    }
+}
diff --git a/main/webapp/modules/core/MOD-INF/controller.js b/main/webapp/modules/core/MOD-INF/controller.js
index c35f1cc37..49ed9f6c3 100644
--- a/main/webapp/modules/core/MOD-INF/controller.js
+++ b/main/webapp/modules/core/MOD-INF/controller.js
@@ -172,6 +172,10 @@ function registerOperations() {
   OR.registerOperation(module, "column-removal", Packages.com.google.refine.operations.column.ColumnRemovalOperation);
   OR.registerOperation(module, "column-rename", Packages.com.google.refine.operations.column.ColumnRenameOperation);
   OR.registerOperation(module, "column-move", Packages.com.google.refine.operations.column.ColumnMoveOperation);
+  OR.registerOperation(module, "column-move-left", Packages.com.google.refine.operations.column.ColumnMoveLeftOperation);
+  OR.registerOperation(module, "column-move-right", Packages.com.google.refine.operations.column.ColumnMoveRightOperation);
+  OR.registerOperation(module, "column-move-first", Packages.com.google.refine.operations.column.ColumnMoveFirstOperation);
+  OR.registerOperation(module, "column-move-last", Packages.com.google.refine.operations.column.ColumnMoveLastOperation);
   OR.registerOperation(module, "column-split", Packages.com.google.refine.operations.column.ColumnSplitOperation);
   OR.registerOperation(module, "column-addition-by-fetching-urls", Packages.com.google.refine.operations.column.ColumnAdditionByFetchingURLsOperation);
   OR.registerOperation(module, "column-reorder", Packages.com.google.refine.operations.column.ColumnReorderOperation);
diff --git a/main/webapp/modules/core/scripts/project/operation-icons.js b/main/webapp/modules/core/scripts/project/operation-icons.js
index 0e46b489f..5414a256f 100644
--- a/main/webapp/modules/core/scripts/project/operation-icons.js
+++ b/main/webapp/modules/core/scripts/project/operation-icons.js
@@ -31,6 +31,10 @@ OperationIconRegistry.setIcon('core/column-addition', 'images/operations/add.svg
 OperationIconRegistry.setIcon('core/column-removal', 'images/operations/delete.svg');
 OperationIconRegistry.setIcon('core/column-rename', 'images/operations/rename.svg');
 OperationIconRegistry.setIcon('core/column-move', 'images/operations/column-move.svg');
+OperationIconRegistry.setIcon('core/column-move-left', 'images/operations/move-left.svg');
+OperationIconRegistry.setIcon('core/column-move-right', 'images/operations/move-right.svg');
+OperationIconRegistry.setIcon('core/column-move-first', 'images/operations/move-first.svg');
+OperationIconRegistry.setIcon('core/column-move-last', 'images/operations/move-last.svg');
 OperationIconRegistry.setIcon('core/column-split', 'images/operations/split-columns.svg');
 OperationIconRegistry.setIcon('core/column-addition-by-fetching-urls', 'images/operations/fetch-urls.svg');
 OperationIconRegistry.setIcon('core/column-reorder', 'images/operations/column-reorder.svg');
diff --git a/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js b/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js
index 5cd17b9ea..16a884faa 100644
--- a/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js
+++ b/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js
@@ -214,33 +214,20 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
     );
   };

-  var doMoveColumnTo = function(index) {
+  var doMoveColumnTo = function(direction) {
     Refine.postCoreProcess(
-      "move-column",
-      {
-        columnName: column.name,
-        index: index
-      },
-      null,
+      "apply-operations",
+      {},
+      { operations: JSON.stringify([
+        {
+           op: 'core/column-move-' + direction,
+           columnName: column.name
+        }
+      ]) },
       { modelsChanged: true, rowIdsPreserved: true }
     );
   };

-  var doMoveColumnBy = function(change) {
-    var newidx = Refine.columnNameToColumnIndex(column.name) + change;
-    if (newidx >= 0) {
-      Refine.postCoreProcess(
-          "move-column",
-          {
-            columnName: column.name,
-            index: newidx
-          },
-          null,
-          { modelsChanged: true, rowIdsPreserved: true }
-      );
-    }
-  };
-
   var doSplitColumn = function() {
     var frame = $(DOM.loadHTML("core", "scripts/views/data-table/split-column-dialog.html"));
     var elmts = DOM.bind(frame);
@@ -566,25 +553,25 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
         id: "core/move-column-to-beginning",
         label: $.i18n('core-views/move-to-beg'),
         icon: 'images/operations/move-first.svg',
-        click: function() { doMoveColumnTo(0); }
+        click: function() { doMoveColumnTo('first'); }
       },
       {
         id: "core/move-column-to-end",
         label: $.i18n('core-views/move-to-end'),
         icon: 'images/operations/move-last.svg',
-        click: function() { doMoveColumnTo(theProject.columnModel.columns.length - 1); }
+        click: function() { doMoveColumnTo('last'); }
       },
       {
         id: "core/move-column-to-left",
         label: $.i18n('core-views/move-to-left'),
         icon: 'images/operations/move-left.svg',
-        click: function() { doMoveColumnBy(-1);}
+        click: function() { doMoveColumnTo('left');}
       },
       {
         id: "core/move-column-to-right",
         label: $.i18n('core-views/move-to-right'),
         icon: 'images/operations/move-right.svg',
-        click: function() { doMoveColumnBy(1); }
+        click: function() { doMoveColumnTo('right'); }
       }
     ]
   );

diff --git a/main/tests/cypress/cypress/e2e/project/grid/column/edit-column/reposition_column.cy.js b/main/tests/cypress/cypress/e2e/project/grid/column/edit-column/reposition_column.cy.js
index 805ca5753..031c6c5c9 100644
--- a/main/tests/cypress/cypress/e2e/project/grid/column/edit-column/reposition_column.cy.js
+++ b/main/tests/cypress/cypress/e2e/project/grid/column/edit-column/reposition_column.cy.js
@@ -7,7 +7,7 @@ describe(__filename, function () {
       'Move column to beginning',
     ]);

-    cy.assertNotificationContainingText('Move column Shrt_Desc to position 0');
+    cy.assertNotificationContainingText('Move column Shrt_Desc to the beginning');

     cy.get('.data-table-header th:nth-child(2)').should(
       'to.contain',
@@ -19,7 +19,7 @@ describe(__filename, function () {

     cy.columnActionClick('Shrt_Desc', ['Edit column', 'Move column to end']);

-    cy.assertNotificationContainingText('Move column Shrt_Desc to position 3');
+    cy.assertNotificationContainingText('Move column Shrt_Desc to the end');

     cy.get('.data-table-header th:nth-child(5)').should(
       'to.contain',
@@ -31,7 +31,7 @@ describe(__filename, function () {

     cy.columnActionClick('Shrt_Desc', ['Edit column', 'Move column left']);

-    cy.assertNotificationContainingText('Move column Shrt_Desc to position 0');
+    cy.assertNotificationContainingText('Move column Shrt_Desc to the left');

     cy.get('.data-table-header th:nth-child(2)').should(
       'to.contain',
@@ -43,7 +43,7 @@ describe(__filename, function () {

     cy.columnActionClick('Shrt_Desc', ['Edit column', 'Move column right']);

-    cy.assertNotificationContainingText('Move column Shrt_Desc to position 2');
+    cy.assertNotificationContainingText('Move column Shrt_Desc to the right');

     cy.get('.data-table-header th:nth-child(4)').should(
       'to.contain',
diff --git a/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellJoinOperationTests.java b/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellJoinOperationTests.java
index 8a080f030..bf4de400d 100644
--- a/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellJoinOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellJoinOperationTests.java
@@ -121,6 +121,11 @@ public class MultiValuedCellJoinOperationTests extends RefineTest {
         assertEquals(new MultiValuedCellJoinOperation("value", "key", "sep").getColumnDependencies().get(), Set.of("value", "key"));
     }

+    @Test
+    public void testColumnsDependenciesWithIdenticalColumns() {
+        assertEquals(new MultiValuedCellJoinOperation("key", "key", "sep").getColumnDependencies().get(), Set.of("key"));
+    }
+
     @Test
     public void testRename() {
         var SUT = new MultiValuedCellJoinOperation("value", "key", "sep");
diff --git a/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellSplitOperationTests.java b/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellSplitOperationTests.java
index ed1adeb1a..d3f7da64b 100644
--- a/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellSplitOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/cell/MultiValuedCellSplitOperationTests.java
@@ -185,6 +185,15 @@ public class MultiValuedCellSplitOperationTests extends RefineTest {
                 false).getColumnDependencies().get(), Set.of("Value", "Key"));
     }

+    @Test
+    public void testColumnsDependenciesWithIdenticalColumns() {
+        assertEquals(new MultiValuedCellSplitOperation(
+                "Key",
+                "Key",
+                ":",
+                false).getColumnDependencies().get(), Set.of("Key"));
+    }
+
     @Test
     public void testRename() {
         var SUT = new MultiValuedCellSplitOperation(
diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnMoveFirstOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnMoveFirstOperationTests.java
new file mode 100644
index 000000000..cbaa3f905
--- /dev/null
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnMoveFirstOperationTests.java
@@ -0,0 +1,111 @@
+
+package com.google.refine.operations.column;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeSuite;
+import org.testng.annotations.Test;
+
+import com.google.refine.RefineTest;
+import com.google.refine.expr.EvalError;
+import com.google.refine.model.AbstractOperation;
+import com.google.refine.model.ColumnsDiff;
+import com.google.refine.model.Project;
+import com.google.refine.operations.OperationDescription;
+import com.google.refine.operations.OperationRegistry;
+import com.google.refine.util.ParsingUtilities;
+import com.google.refine.util.TestUtils;
+
+public class ColumnMoveFirstOperationTests extends RefineTest {
+
+    protected Project project;
+
+    @BeforeMethod
+    public void setUpInitialState() {
+        project = createProject(new String[] { "foo", "bar", "hello" },
+                new Serializable[][] {
+                        { "v1", "a", "d" },
+                        { "v3", "a", "f" },
+                        { "", "a", "g" },
+                        { "", "b", "h" },
+                        { new EvalError("error"), "a", "i" },
+                        { "v1", "b", "j" }
+                });
+    }
+
+    @BeforeSuite
+    public void setUp() {
+        OperationRegistry.registerOperation(getCoreModule(), "column-move-first", ColumnMoveFirstOperation.class);
+    }
+
+    @Test
+    public void serializeColumnMoveFirstOperation() throws Exception {
+        String json = "{\"op\":\"core/column-move-first\","
+                + "\"description\":" + new TextNode(OperationDescription.column_move_first_brief("my column")).toString() + ","
+                + "\"columnName\":\"my column\"}";
+        TestUtils.isSerializedTo(ParsingUtilities.mapper.readValue(json, ColumnMoveFirstOperation.class), json);
+    }
+
+    @Test
+    public void testValidate() {
+        AbstractOperation missingColumnName = new ColumnMoveFirstOperation(null);
+        assertThrows(IllegalArgumentException.class, () -> missingColumnName.validate());
+    }
+
+    @Test
+    public void testMove() throws Exception {
+        ColumnMoveFirstOperation operation = new ColumnMoveFirstOperation("hello");
+        assertEquals(operation.getColumnDependencies().get(), Set.of("hello"));
+        assertEquals(operation.getColumnsDiff().get(), ColumnsDiff.empty());
+
+        runOperation(operation, project);
+
+        Project expected = createProject(new String[] { "hello", "foo", "bar" },
+                new Serializable[][] {
+                        { "d", "v1", "a" },
+                        { "f", "v3", "a" },
+                        { "g", "", "a" },
+                        { "h", "", "b" },
+                        { "i", new EvalError("error"), "a" },
+                        { "j", "v1", "b" }
+                });
+        assertProjectEquals(project, expected);
+    }
+
+    @Test
+    public void testSamePosition() throws Exception {
+        AbstractOperation SUT = new ColumnMoveFirstOperation("foo");
+        assertEquals(SUT.getColumnDependencies().get(), Set.of("foo"));
+        assertEquals(SUT.getColumnsDiff().get(), ColumnsDiff.empty());
+
+        runOperation(SUT, project);
+
+        Project expected = createProject(
+                new String[] { "foo", "bar", "hello" },
+                new Serializable[][] {
+                        { "v1", "a", "d" },
+                        { "v3", "a", "f" },
+                        { "", "a", "g" },
+                        { "", "b", "h" },
+                        { new EvalError("error"), "a", "i" },
+                        { "v1", "b", "j" },
+                });
+        assertProjectEquals(project, expected);
+    }
+
+    @Test
+    public void testRename() {
+        ColumnMoveFirstOperation SUT = new ColumnMoveFirstOperation("hello");
+
+        ColumnMoveFirstOperation renamed = SUT.renameColumns(Map.of("hello", "world"));
+
+        assertEquals(renamed._columnName, "world");
+    }
+}
diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnMoveLastOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnMoveLastOperationTests.java
new file mode 100644
index 000000000..318011c01
--- /dev/null
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnMoveLastOperationTests.java
@@ -0,0 +1,111 @@
+
+package com.google.refine.operations.column;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeSuite;
+import org.testng.annotations.Test;
+
+import com.google.refine.RefineTest;
+import com.google.refine.expr.EvalError;
+import com.google.refine.model.AbstractOperation;
+import com.google.refine.model.ColumnsDiff;
+import com.google.refine.model.Project;
+import com.google.refine.operations.OperationDescription;
+import com.google.refine.operations.OperationRegistry;
+import com.google.refine.util.ParsingUtilities;
+import com.google.refine.util.TestUtils;
+
+public class ColumnMoveLastOperationTests extends RefineTest {
+
+    protected Project project;
+
+    @BeforeMethod
+    public void setUpInitialState() {
+        project = createProject(new String[] { "foo", "bar", "hello" },
+                new Serializable[][] {
+                        { "v1", "a", "d" },
+                        { "v3", "a", "f" },
+                        { "", "a", "g" },
+                        { "", "b", "h" },
+                        { new EvalError("error"), "a", "i" },
+                        { "v1", "b", "j" }
+                });
+    }
+
+    @BeforeSuite
+    public void setUp() {
+        OperationRegistry.registerOperation(getCoreModule(), "column-move-last", ColumnMoveLastOperation.class);
+    }
+
+    @Test
+    public void serializeColumnMoveLastOperation() throws Exception {
+        String json = "{\"op\":\"core/column-move-last\","
+                + "\"description\":" + new TextNode(OperationDescription.column_move_last_brief("my column")).toString() + ","
+                + "\"columnName\":\"my column\"}";
+        TestUtils.isSerializedTo(ParsingUtilities.mapper.readValue(json, ColumnMoveLastOperation.class), json);
+    }
+
+    @Test
+    public void testValidate() {
+        AbstractOperation missingColumnName = new ColumnMoveLastOperation(null);
+        assertThrows(IllegalArgumentException.class, () -> missingColumnName.validate());
+    }
+
+    @Test
+    public void testMove() throws Exception {
+        ColumnMoveLastOperation operation = new ColumnMoveLastOperation("foo");
+        assertEquals(operation.getColumnDependencies().get(), Set.of("foo"));
+        assertEquals(operation.getColumnsDiff().get(), ColumnsDiff.empty());
+
+        runOperation(operation, project);
+
+        Project expected = createProject(new String[] { "bar", "hello", "foo" },
+                new Serializable[][] {
+                        { "a", "d", "v1" },
+                        { "a", "f", "v3" },
+                        { "a", "g", "" },
+                        { "b", "h", "" },
+                        { "a", "i", new EvalError("error") },
+                        { "b", "j", "v1" }
+                });
+        assertProjectEquals(project, expected);
+    }
+
+    @Test
+    public void testSamePosition() throws Exception {
+        AbstractOperation SUT = new ColumnMoveRightOperation("hello");
+        assertEquals(SUT.getColumnDependencies().get(), Set.of("hello"));
+        assertEquals(SUT.getColumnsDiff().get(), ColumnsDiff.empty());
+
+        runOperation(SUT, project);
+
+        Project expected = createProject(
+                new String[] { "foo", "bar", "hello" },
+                new Serializable[][] {
+                        { "v1", "a", "d" },
+                        { "v3", "a", "f" },
+                        { "", "a", "g" },
+                        { "", "b", "h" },
+                        { new EvalError("error"), "a", "i" },
+                        { "v1", "b", "j" },
+                });
+        assertProjectEquals(project, expected);
+    }
+
+    @Test
+    public void testRename() {
+        ColumnMoveLastOperation SUT = new ColumnMoveLastOperation("hello");
+
+        ColumnMoveLastOperation renamed = SUT.renameColumns(Map.of("hello", "world"));
+
+        assertEquals(renamed._columnName, "world");
+    }
+}
diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnMoveLeftOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnMoveLeftOperationTests.java
new file mode 100644
index 000000000..5c531a11c
--- /dev/null
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnMoveLeftOperationTests.java
@@ -0,0 +1,112 @@
+
+package com.google.refine.operations.column;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeSuite;
+import org.testng.annotations.Test;
+
+import com.google.refine.RefineTest;
+import com.google.refine.expr.EvalError;
+import com.google.refine.model.AbstractOperation;
+import com.google.refine.model.ColumnsDiff;
+import com.google.refine.model.Project;
+import com.google.refine.operations.OperationDescription;
+import com.google.refine.operations.OperationRegistry;
+import com.google.refine.util.ParsingUtilities;
+import com.google.refine.util.TestUtils;
+
+public class ColumnMoveLeftOperationTests extends RefineTest {
+
+    protected Project project;
+
+    @BeforeMethod
+    public void setUpInitialState() {
+        project = createProject(new String[] { "foo", "bar", "hello" },
+                new Serializable[][] {
+                        { "v1", "a", "d" },
+                        { "v3", "a", "f" },
+                        { "", "a", "g" },
+                        { "", "b", "h" },
+                        { new EvalError("error"), "a", "i" },
+                        { "v1", "b", "j" }
+                });
+    }
+
+    @BeforeSuite
+    public void setUp() {
+        OperationRegistry.registerOperation(getCoreModule(), "column-move-left", ColumnMoveLeftOperation.class);
+    }
+
+    @Test
+    public void serializeColumnMoveLeftOperation() throws Exception {
+        String json = "{\"op\":\"core/column-move-left\","
+                + "\"description\":" + new TextNode(OperationDescription.column_move_left_brief("my column")).toString() + ","
+                + "\"columnName\":\"my column\"}";
+        TestUtils.isSerializedTo(ParsingUtilities.mapper.readValue(json, ColumnMoveLeftOperation.class), json);
+    }
+
+    @Test
+    public void testValidate() {
+        AbstractOperation missingColumnName = new ColumnMoveLeftOperation(null);
+        assertThrows(IllegalArgumentException.class, () -> missingColumnName.validate());
+    }
+
+    @Test
+    public void testMove() throws Exception {
+        ColumnMoveLeftOperation operation = new ColumnMoveLeftOperation("bar");
+        assertEquals(operation.getColumnDependencies().get(), Set.of("bar"));
+        assertEquals(operation.getColumnsDiff().get(), ColumnsDiff.empty());
+
+        runOperation(operation, project);
+
+        Project expected = createProject(
+                new String[] { "bar", "foo", "hello" },
+                new Serializable[][] {
+                        { "a", "v1", "d" },
+                        { "a", "v3", "f" },
+                        { "a", "", "g" },
+                        { "b", "", "h" },
+                        { "a", new EvalError("error"), "i" },
+                        { "b", "v1", "j" },
+                });
+        assertProjectEquals(project, expected);
+    }
+
+    @Test
+    public void testSamePosition() throws Exception {
+        AbstractOperation SUT = new ColumnMoveLeftOperation("foo");
+        assertEquals(SUT.getColumnDependencies().get(), Set.of("foo"));
+        assertEquals(SUT.getColumnsDiff().get(), ColumnsDiff.empty());
+
+        runOperation(SUT, project);
+
+        Project expected = createProject(
+                new String[] { "foo", "bar", "hello" },
+                new Serializable[][] {
+                        { "v1", "a", "d" },
+                        { "v3", "a", "f" },
+                        { "", "a", "g" },
+                        { "", "b", "h" },
+                        { new EvalError("error"), "a", "i" },
+                        { "v1", "b", "j" },
+                });
+        assertProjectEquals(project, expected);
+    }
+
+    @Test
+    public void testRename() {
+        ColumnMoveLeftOperation SUT = new ColumnMoveLeftOperation("hello");
+
+        ColumnMoveLeftOperation renamed = SUT.renameColumns(Map.of("hello", "world"));
+
+        assertEquals(renamed._columnName, "world");
+    }
+}
diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnMoveRightOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnMoveRightOperationTests.java
new file mode 100644
index 000000000..0fe64d7bf
--- /dev/null
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnMoveRightOperationTests.java
@@ -0,0 +1,112 @@
+
+package com.google.refine.operations.column;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeSuite;
+import org.testng.annotations.Test;
+
+import com.google.refine.RefineTest;
+import com.google.refine.expr.EvalError;
+import com.google.refine.model.AbstractOperation;
+import com.google.refine.model.ColumnsDiff;
+import com.google.refine.model.Project;
+import com.google.refine.operations.OperationDescription;
+import com.google.refine.operations.OperationRegistry;
+import com.google.refine.util.ParsingUtilities;
+import com.google.refine.util.TestUtils;
+
+public class ColumnMoveRightOperationTests extends RefineTest {
+
+    protected Project project;
+
+    @BeforeMethod
+    public void setUpInitialState() {
+        project = createProject(new String[] { "foo", "bar", "hello" },
+                new Serializable[][] {
+                        { "v1", "a", "d" },
+                        { "v3", "a", "f" },
+                        { "", "a", "g" },
+                        { "", "b", "h" },
+                        { new EvalError("error"), "a", "i" },
+                        { "v1", "b", "j" }
+                });
+    }
+
+    @BeforeSuite
+    public void setUp() {
+        OperationRegistry.registerOperation(getCoreModule(), "column-move-right", ColumnMoveRightOperation.class);
+    }
+
+    @Test
+    public void serializeColumnMoveRightOperation() throws Exception {
+        String json = "{\"op\":\"core/column-move-right\","
+                + "\"description\":" + new TextNode(OperationDescription.column_move_right_brief("my column")).toString() + ","
+                + "\"columnName\":\"my column\"}";
+        TestUtils.isSerializedTo(ParsingUtilities.mapper.readValue(json, ColumnMoveRightOperation.class), json);
+    }
+
+    @Test
+    public void testValidate() {
+        AbstractOperation missingColumnName = new ColumnMoveRightOperation(null);
+        assertThrows(IllegalArgumentException.class, () -> missingColumnName.validate());
+    }
+
+    @Test
+    public void testMove() throws Exception {
+        ColumnMoveRightOperation operation = new ColumnMoveRightOperation("foo");
+        assertEquals(operation.getColumnDependencies().get(), Set.of("foo"));
+        assertEquals(operation.getColumnsDiff().get(), ColumnsDiff.empty());
+
+        runOperation(operation, project);
+
+        Project expected = createProject(
+                new String[] { "bar", "foo", "hello" },
+                new Serializable[][] {
+                        { "a", "v1", "d" },
+                        { "a", "v3", "f" },
+                        { "a", "", "g" },
+                        { "b", "", "h" },
+                        { "a", new EvalError("error"), "i" },
+                        { "b", "v1", "j" },
+                });
+        assertProjectEquals(project, expected);
+    }
+
+    @Test
+    public void testSamePosition() throws Exception {
+        AbstractOperation SUT = new ColumnMoveRightOperation("hello");
+        assertEquals(SUT.getColumnDependencies().get(), Set.of("hello"));
+        assertEquals(SUT.getColumnsDiff().get(), ColumnsDiff.empty());
+
+        runOperation(SUT, project);
+
+        Project expected = createProject(
+                new String[] { "foo", "bar", "hello" },
+                new Serializable[][] {
+                        { "v1", "a", "d" },
+                        { "v3", "a", "f" },
+                        { "", "a", "g" },
+                        { "", "b", "h" },
+                        { new EvalError("error"), "a", "i" },
+                        { "v1", "b", "j" },
+                });
+        assertProjectEquals(project, expected);
+    }
+
+    @Test
+    public void testRename() {
+        ColumnMoveRightOperation SUT = new ColumnMoveRightOperation("hello");
+
+        ColumnMoveRightOperation renamed = SUT.renameColumns(Map.of("hello", "world"));
+
+        assertEquals(renamed._columnName, "world");
+    }
+}

Fixes #7147.

<s>The Cypress tests will fail because they depend on #7173.</s> This is because I didn't introduce new Command classes for the new operations (as it would mean 4 more commands and 4 more command tests), but instead used the `apply-operations` command. I intend to propose to migrate to using `apply-operations` everywhere and deprecate the operation commands in the future.

Improve generalizability of the "column move" operation