=== modified file 'src/main/org/h2/command/dml/Insert.java'
--- old/src/main/org/h2/command/dml/Insert.java	2020-10-15 15:02:18 +0000
+++ new/src/main/org/h2/command/dml/Insert.java	2022-12-02 14:58:43 +0000
@@ -184,7 +184,7 @@
                     }
                 }
                 rowNumber++;
-                table.validateConvertUpdateSequence(session, newRow);
+                table.validateConvertUpdateSequence(session, newRow, null);
                 if (deltaChangeCollectionMode == ResultOption.NEW) {
                     deltaChangeCollector.addRow(newRow.getValueList().clone());
                 }
@@ -246,7 +246,7 @@
         for (int j = 0, len = columns.length; j < len; j++) {
             newRow.setValue(columns[j].getColumnId(), values[j]);
         }
-        table.validateConvertUpdateSequence(session, newRow);
+        table.validateConvertUpdateSequence(session, newRow, null);
         if (deltaChangeCollectionMode == ResultOption.NEW) {
             deltaChangeCollector.addRow(newRow.getValueList().clone());
         }

=== modified file 'src/main/org/h2/command/dml/Merge.java'
--- old/src/main/org/h2/command/dml/Merge.java	2020-10-15 15:02:18 +0000
+++ new/src/main/org/h2/command/dml/Merge.java	2022-12-02 14:58:33 +0000
@@ -172,7 +172,7 @@
         // if update fails try an insert
         if (count == 0) {
             try {
-                table.validateConvertUpdateSequence(session, row);
+                table.validateConvertUpdateSequence(session, row, null);
                 if (deltaChangeCollectionMode == ResultOption.NEW) {
                     deltaChangeCollector.addRow(row.getValueList().clone());
                 }

=== modified file 'src/main/org/h2/command/dml/Update.java'
--- old/src/main/org/h2/command/dml/Update.java	2020-10-15 15:02:18 +0000
+++ new/src/main/org/h2/command/dml/Update.java	2022-12-02 14:00:40 +0000
@@ -5,8 +5,11 @@
  */
 package org.h2.command.dml;
 
+import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 
@@ -21,6 +24,7 @@
 import org.h2.expression.ExpressionVisitor;
 import org.h2.expression.Parameter;
 import org.h2.expression.ValueExpression;
+import org.h2.index.Index;
 import org.h2.message.DbException;
 import org.h2.result.ResultInterface;
 import org.h2.result.ResultTarget;
@@ -116,8 +120,14 @@
     public int update() {
         targetTableFilter.startQuery(session);
         targetTableFilter.reset();
-        try (RowList rows = new RowList(session)) {
+        try (RowList rows = new RowList(session);
+             RowList inMemRows = new RowList(session)) {
             Table table = targetTableFilter.getTable();
+            
+            // this is enabled only for PageStore and in-mem databases;  in PageStore, the first index in the
+            // list is the ScanIndex, which is not included in this optimization.
+            boolean inMemUpdate = table.useInPlaceUpdate();
+            
             session.getUser().checkRight(table, Right.UPDATE);
             table.fire(session, Trigger.UPDATE, true);
             table.lock(session, true, false);
@@ -133,6 +143,9 @@
                     limitRows = v.getInt();
                 }
             }
+
+            BitSet dirtyCols = inMemUpdate ? new BitSet(columnCount) : null;
+            
             while (targetTableFilter.next()) {
                 setCurrentRowNumber(count+1);
                 if (limitRows >= 0 && count >= limitRows) {
@@ -154,29 +167,59 @@
                         }
                     }
                     Row newRow = table.getTemplateRow();
-                    boolean setOnUpdate = false;
-                    for (int i = 0; i < columnCount; i++) {
-                        Column column = columns[i];
-                        Expression newExpr = setClauseMap.get(column);
-                        Value newValue;
-                        if (newExpr == null) {
-                            if (column.getOnUpdateExpression() != null) {
-                                setOnUpdate = true;
-                            }
-                            newValue = oldRow.getValue(i);
-                        } else if (newExpr == ValueExpression.getDefault()) {
-                            newValue = table.getDefaultValue(session, column);
-                        } else {
-                            newValue = newExpr.getValue(session);
-                        }
-                        newRow.setValue(i, newValue);
-                    }
+                    newRow.copy(oldRow);
+                    
+                    Row oldRowCopy = null;
+                    if (inMemUpdate)
+                    {
+                       // create a copy for the triggers
+                       oldRowCopy = table.getTemplateRow();
+                       oldRowCopy.copy(oldRow);
+                       oldRowCopy.setKey(oldRow.getKey());
+                    }
+                    
+                    BitSet onUpdateColumns = null;
+                    if (table.getOnUpdateColumns() != null && !table.getOnUpdateColumns().isEmpty())
+                    {
+                       onUpdateColumns = new BitSet(columnCount);
+                       ArrayList<Integer> onUpdate = table.getOnUpdateColumns();
+                       for (int i = 0; i < onUpdate.size(); i++)
+                       {
+                          onUpdateColumns.set(onUpdate.get(i));
+                       }
+                    }
+                    BitSet changed = new BitSet(columnCount);
+                    
+                    for (Map.Entry<Column, Expression> entry : setClauseMap.entrySet())
+                    {
+                       Column column = entry.getKey();
+                       Expression newExpr = entry.getValue();
+                       Value newValue;
+                       if (newExpr == ValueExpression.getDefault()) 
+                       {
+                          newValue = table.getDefaultValue(session, column);
+                       }
+                       else 
+                       {
+                          newValue = newExpr.getValue(session);
+                       }
+                       newRow.setValue(column.getColumnId(), newValue);
+                       if (column.getColumnId() >= 0) // there are negative-ID columns like '_rowid_'
+                       {
+                          changed.set(column.getColumnId());
+                          if (onUpdateColumns != null)
+                          {
+                             onUpdateColumns.clear(column.getColumnId());
+                          }
+                       }
+                    }
+                    boolean setOnUpdate = onUpdateColumns != null && !onUpdateColumns.isEmpty();
                     long key = oldRow.getKey();
                     newRow.setKey(key);
-                    table.validateConvertUpdateSequence(session, newRow);
+                    table.validateConvertUpdateSequence(session, newRow, changed);
                     if (setOnUpdate || updateToCurrentValuesReturnsZero) {
                         setOnUpdate = false;
-                        for (int i = 0; i < columnCount; i++) {
+                        for (int i = changed.nextSetBit(0); i >= 0; i = changed.nextSetBit(i + 1)) {
                             // Use equals here to detect changes from numeric 0 to 0.0 and similar
                             if (!Objects.equals(oldRow.getValue(i), newRow.getValue(i))) {
                                 setOnUpdate = true;
@@ -184,13 +227,11 @@
                             }
                         }
                         if (setOnUpdate) {
-                            for (int i = 0; i < columnCount; i++) {
-                                Column column = columns[i];
-                                if (setClauseMap.get(column) == null) {
-                                    if (column.getOnUpdateExpression() != null) {
-                                        newRow.setValue(i, table.getOnUpdateValue(session, column));
-                                    }
-                                }
+                           if (onUpdateColumns != null)
+                             for (int i = onUpdateColumns.nextSetBit(0); i >= 0; i = onUpdateColumns.nextSetBit(i + 1)) {
+                               Column column = columns[i];
+                               newRow.setValue(i, table.getOnUpdateValue(session, column));
+                               changed.set(i);
                             }
                         } else if (updateToCurrentValuesReturnsZero) {
                             count--;
@@ -202,8 +243,23 @@
                         deltaChangeCollector.addRow(newRow.getValueList().clone());
                     }
                     if (!table.fireRow() || !table.fireBeforeRow(session, oldRow, newRow)) {
-                        rows.add(oldRow);
-                        rows.add(newRow);
+                       
+                        if (inMemUpdate)
+                        {
+                           dirtyCols.or(changed);
+                           
+                           inMemRows.add(oldRow);
+                           inMemRows.add(newRow);
+                           
+                           rows.add(oldRowCopy);
+                           rows.add(newRow);
+                        }
+                        else
+                        {
+                           rows.add(oldRow);
+                           rows.add(newRow);
+                        }
+                       
                         if (updatedKeysCollector != null) {
                             updatedKeysCollector.add(key);
                         }
@@ -221,8 +277,46 @@
             // TODO update in-place (but if the key changes,
             // we need to update all indexes) before row triggers
 
-            // the cached row is already updated - we need the old values
-            table.updateRows(this, session, rows);
+            boolean updateRows = !inMemUpdate;
+            
+            if (!updateRows)
+            {
+               ArrayList<Index> indexes = table.getIndexes();
+               // skip scan index
+               for (int i = 1; !updateRows && i < indexes.size(); i++)
+               {
+                  Index index = indexes.get(i);
+                  Column[] idxColumns = index.getColumns();
+                  for (int j = 0; j < idxColumns.length; j++)
+                  {
+                     if (dirtyCols.get(idxColumns[j].getColumnId()))
+                     {
+                        // if there is any touched index in this update, then use the 'updateRows' approach
+                        // this is required because for in-memory database, a single row instance is kept
+                        // everywhere, and the index update can be done only by removing and adding again the
+                        // row
+                        
+                        updateRows = true;
+                        break;
+                     }
+                  }
+               }
+            }
+            
+            if (updateRows)
+            {
+               table.updateRows(this, session, rows);
+            }
+            else
+            {
+               for (inMemRows.reset(); inMemRows.hasNext();) {
+                  // copy the old row to new, as no index is affected
+                  Row o = inMemRows.next();
+                  Row n = inMemRows.next();
+                  o.copy(n);
+              }
+            }
+
             if (table.fireRow()) {
                 for (rows.reset(); rows.hasNext();) {
                     Row o = rows.next();

=== modified file 'src/main/org/h2/pagestore/db/PageStoreTable.java'
--- old/src/main/org/h2/pagestore/db/PageStoreTable.java	2020-10-15 15:35:20 +0000
+++ new/src/main/org/h2/pagestore/db/PageStoreTable.java	2022-12-02 14:00:36 +0000
@@ -66,6 +66,12 @@
         indexes.add(scanIndex);
         traceLock = database.getTrace(Trace.LOCK);
     }
+    
+    @Override
+    public boolean useInPlaceUpdate()
+    {
+       return !database.isPersistent() && !database.isMVStore();
+    }
 
     @Override
     public void close(Session session) {

=== modified file 'src/main/org/h2/pagestore/db/TreeIndex.java'
--- old/src/main/org/h2/pagestore/db/TreeIndex.java	2020-10-15 15:02:18 +0000
+++ new/src/main/org/h2/pagestore/db/TreeIndex.java	2022-11-28 10:36:42 +0000
@@ -5,6 +5,8 @@
  */
 package org.h2.pagestore.db;
 
+import java.util.HashMap;
+
 import org.h2.command.dml.AllColumnsForPlan;
 import org.h2.engine.Session;
 import org.h2.index.BaseIndex;
@@ -25,6 +27,7 @@
 public class TreeIndex extends BaseIndex {
 
     private TreeNode root;
+    private HashMap<Long, TreeNode> byKey = new HashMap<>();
     private final PageStoreTable tableData;
     private long rowCount;
     private boolean closed;
@@ -41,6 +44,7 @@
     @Override
     public void close(Session session) {
         root = null;
+        byKey.clear();
         closed = true;
     }
 
@@ -50,6 +54,7 @@
             throw DbException.throwInternalError();
         }
         TreeNode i = new TreeNode(row);
+        byKey.put(row.getKey(), i);
         TreeNode n = root, x = n;
         boolean isLeft = true;
         while (true) {
@@ -157,6 +162,8 @@
         if (x == null) {
             throw DbException.throwInternalError("not found!");
         }
+        byKey.remove(row.getKey());
+        
         TreeNode n;
         if (x.left == null) {
             n = x.right;
@@ -272,6 +279,13 @@
     }
 
     private TreeNode findFirstNode(SearchRow row, boolean withKey) {
+       TreeNode k = null; 
+       if (withKey)
+       {
+          k = byKey.get(row.getKey());
+          return k;
+       }
+       
         TreeNode x = root, result = x;
         while (x != null) {
             result = x;
@@ -334,6 +348,7 @@
     @Override
     public void truncate(Session session) {
         root = null;
+        byKey.clear();
         rowCount = 0;
     }
 

=== modified file 'src/main/org/h2/result/Row.java'
--- old/src/main/org/h2/result/Row.java	2020-10-15 15:02:18 +0000
+++ new/src/main/org/h2/result/Row.java	2022-12-02 15:58:45 +0000
@@ -66,4 +66,13 @@
      */
     boolean hasSharedData(Row other);
 
+    
+    /**
+     * Perform a direct copy of the old row's data to this row.
+     * 
+     * @param oldRow
+     *        The row to copy from.
+     */
+    void copy(Row oldRow);
+
 }

=== modified file 'src/main/org/h2/result/RowImpl.java'
--- old/src/main/org/h2/result/RowImpl.java	2020-10-15 15:02:18 +0000
+++ new/src/main/org/h2/result/RowImpl.java	2022-12-02 15:59:02 +0000
@@ -25,6 +25,11 @@
     }
 
     @Override
+    public void copy(Row src) {
+        System.arraycopy(((RowImpl) src).data, 0, data, 0, data.length);
+    }
+    
+    @Override
     public void setKey(SearchRow row) {
         setKey(row.getKey());
     }

=== modified file 'src/main/org/h2/table/Table.java'
--- old/src/main/org/h2/table/Table.java	2020-11-13 14:04:32 +0000
+++ new/src/main/org/h2/table/Table.java	2022-12-02 16:02:02 +0000
@@ -62,7 +62,12 @@
     /**
      * The indexes of the computed columns
      */
-    protected List<Integer> computedColumns = new ArrayList<>();
+    protected List<Integer> computedColumns = null;
+    
+    /**
+     * The indexes of the columns with ON UPDATE clauses. 
+     */
+    private ArrayList<Integer> onUpdateColumns = null;
 
     /**
      * The compare mode used for this table.
@@ -272,6 +277,45 @@
     public abstract ArrayList<Index> getIndexes();
 
     /**
+     * Get all the indexes which use the specified columns.
+     * 
+     * @param columns
+     *        The column IDs.
+     * @return The list of indexes which use these columns.
+     */
+    public ArrayList<Index> getIndexes(BitSet columns)
+    {
+       ArrayList<Index> result = new ArrayList<>();
+       
+       ArrayList<Index> indexes = getIndexes();
+       for (int i = 0; i < indexes.size(); i++)
+       {
+          Index index = indexes.get(i);
+          Column[] cols = index.getColumns();
+          for (int j = 0; j < cols.length; j++)
+          {
+             if (columns.get(cols[j].getColumnId()))
+             {
+                result.add(index);
+                break;
+             }
+          }
+       }
+       
+       return result;
+    }
+
+    /**
+     * Get all the ON UPDATE columns.
+     * 
+     * @return The {@link #onUpdateColumns}.
+     */
+    public ArrayList<Integer> getOnUpdateColumns()
+    {
+       return onUpdateColumns;
+    }
+    
+    /**
      * Get an index by name.
      *
      * @param indexName the index name to search for
@@ -351,6 +395,16 @@
     public abstract long getDiskSpaceUsed();
 
     /**
+     * Check if this table can use in-place row copy with the UPDATE statement.
+     * 
+     * @return <code>false</code> by default.
+     */
+    public boolean useInPlaceUpdate()
+    {
+       return false;
+    }
+    
+    /**
      * Get the row id column if this table has one.
      *
      * @return the row id column, or null
@@ -435,9 +489,13 @@
         if (columnMap.size() > 0) {
             columnMap.clear();
         }
-        if (computedColumns.size() > 0) {
+        if (computedColumns != null && computedColumns.size() > 0) {
            computedColumns.clear();
         }
+        if (onUpdateColumns != null && onUpdateColumns.size() > 0)
+        {
+           onUpdateColumns.clear();
+        }
         
         for (int i = 0; i < columns.length; i++) {
             Column col = columns[i];
@@ -453,8 +511,20 @@
             }
             columnMap.put(columnName, col);
             if (col.getComputed()) {
+               if (computedColumns == null)
+               {
+                  computedColumns = new ArrayList<>();
+               }
                computedColumns.add(i);
             }
+            if (col.getOnUpdateExpression() != null)
+            {
+               if (onUpdateColumns == null)
+               {
+                  onUpdateColumns = new ArrayList<>();
+               }
+               onUpdateColumns.add(i);
+            }
         }
     }
 
@@ -839,15 +909,36 @@
      *
      * @param session the session
      * @param row the row
+     * @param setCols When set, it contains the indexes of the columns changed via the UPDATE ... SET statement.
      */
-    public void validateConvertUpdateSequence(Session session, Row row) {
-        for (int i = 0; i < computedColumns.size(); i++)
-        {
-            int idx = computedColumns.get(i);
-            row.setValue(idx, columns[idx].computeValue(session, row));
-        }
-        
+    public void validateConvertUpdateSequence(Session session, Row row, BitSet setCols) {
+       BitSet changed = null;
+       if (setCols != null)
+       {
+          changed = new BitSet();
+          changed.or(setCols);
+       }
+       
+       if (computedColumns != null)
+       {
+           for (int i = 0; i < computedColumns.size(); i++)
+           {
+               int idx = computedColumns.get(i);
+               row.setValue(idx, columns[idx].computeValue(session, row));
+               if (changed != null)
+               {
+                  changed.set(idx);
+               }
+           }
+       }
+       
         for (int i = 0; i < columns.length; i++) {
+           if (changed != null && !changed.get(i))
+           {
+              // only changed columns
+              continue;
+           }
+           
            Value value = row.getValue(i);
            Value v2 = columns[i].validateConvertUpdateSequence(session, value);
            if (v2 != value) {

=== modified file 'src/main/org/h2/table/TableLink.java'
--- old/src/main/org/h2/table/TableLink.java	2020-10-15 15:02:18 +0000
+++ new/src/main/org/h2/table/TableLink.java	2022-12-02 16:02:23 +0000
@@ -13,6 +13,7 @@
 import java.sql.Statement;
 import java.sql.Types;
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
@@ -694,9 +695,10 @@
      *
      * @param session the session
      * @param row the row
+     * @param setCols When set, it contains the indexes of the columns changed via the UPDATE ... SET statement.
      */
     @Override
-    public void validateConvertUpdateSequence(Session session, Row row) {
+    public void validateConvertUpdateSequence(Session session, Row row, BitSet setCols) {
         for (int i = 0; i < columns.length; i++) {
             Value value = row.getValue(i);
             if (value != null) {

