Add realm id column to offline_client_session table

Closes #44424

Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
This commit is contained in:
Pedro Ruivo
2026-01-23 15:28:34 +00:00
committed by GitHub
parent fcdc03a2f6
commit 2f4f36eabc
6 changed files with 182 additions and 15 deletions

View File

@@ -65,6 +65,13 @@ It does not affect any explicitly configured clock skew for example in identity
All the `base` themes are now tagged as **abstract**, and they are not listed in the admin console to be selected (**Realm Settings** -> **Themes** tab). They were always intended to be only extended but not used directly. If you use one of them, it will continue working (or not working) in the same way but cannot be selected using the admin console anymore. Please select one of the available default themes or create your own one.
=== New database indexes on the `OFFLINE_CLIENT_SESSION` table
The `OFFLINE_CLIENT_SESSION` table now contains two additional index `IDX_OFFLINE_CSS_BY_CLIENT_AND_REALM` and `IDX_OFFLINE_CSS_BY_USER_SESSION_AND_OFFLINE` to improve performance.
If the table contains more than 300000 entries, {project_name} will skip the index creation by default during the automatic schema migration and instead log the SQL statement on the console during migration to be applied manually after {project_name}'s startup.
See the link:{upgradingguide_link}[{upgradingguide_name}] for details on how to configure a different limit.
// ------------------------ Deprecated features ------------------------ //
== Deprecated features

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.connections.jpa.updater.liquibase.custom;
import liquibase.database.core.MSSQLDatabase;
import liquibase.database.core.MySQLDatabase;
import liquibase.database.core.OracleDatabase;
import liquibase.database.core.PostgresDatabase;
import liquibase.exception.CustomChangeException;
import liquibase.statement.core.RawParameterizedSqlStatement;
public class JpaUpdate26_6_0_OfflineClientSessionRealm extends CustomKeycloakTask {
@Override
protected void generateStatementsImpl() throws CustomChangeException {
String clientSessionTable = getTableName("OFFLINE_CLIENT_SESSION");
String userSessionTable = getTableName("OFFLINE_USER_SESSION");
if (database instanceof PostgresDatabase) {
generateUpdateQueryForPostgresSQL(clientSessionTable, userSessionTable);
return;
}
if (database instanceof MySQLDatabase) {
// and MariaDB
generateUpdateQueryForMySQL(clientSessionTable, userSessionTable);
return;
}
if (database instanceof MSSQLDatabase) {
generateUpdateQueryForMSSQL(clientSessionTable, userSessionTable);
return;
}
if (database instanceof OracleDatabase) {
generateUpdateQueryForOracle(clientSessionTable, userSessionTable);
return;
}
// H2 and others, very slow with O(n^2) complexity
// It is standard SQL queries, it *must* be compatible with all vendors (fingers crossed)
generateUpdateQueryUsingStandardSQL(clientSessionTable, userSessionTable);
}
@Override
protected String getTaskId() {
return "Sets the realm column in offline_client_session";
}
private void generateUpdateQueryUsingStandardSQL(String clientSessionTable, String userSessionTable) {
statements.add(new RawParameterizedSqlStatement("""
UPDATE %s cs
SET cs.realm_id = (
SELECT us.realm_id
FROM %s us
WHERE us.user_session_id = cs.user_session_id AND cs.offline_flag = us.offline_flag
)
WHERE cs.realm_id IS NULL"""
.formatted(clientSessionTable, userSessionTable)));
}
private void generateUpdateQueryForOracle(String clientSessionTable, String userSessionTable) {
statements.add(new RawParameterizedSqlStatement("""
MERGE INTO %s cs
USING %s us
ON (cs.user_session_id = us.user_session_id AND cs.offline_flag = us.offline_flag)
WHEN MATCHED THEN
UPDATE SET cs.realm_id = us.realm_id"""
.formatted(clientSessionTable, userSessionTable)));
}
private void generateUpdateQueryForMSSQL(String clientSessionTable, String userSessionTable) {
statements.add(new RawParameterizedSqlStatement("""
UPDATE cs
SET cs.realm_id = us.realm_id
FROM %s cs
INNER JOIN %s us ON cs.user_session_id = us.user_session_id AND cs.offline_flag = us.offline_flag AND cs.realm_id IS NULL"""
.formatted(clientSessionTable, userSessionTable)));
}
private void generateUpdateQueryForMySQL(String clientSessionTable, String userSessionTable) {
statements.add(new RawParameterizedSqlStatement("""
UPDATE %s cs
INNER JOIN %s us ON cs.user_session_id = us.user_session_id AND cs.offline_flag = us.offline_flag AND cs.realm_id IS NULL
SET cs.realm_id = us.realm_id"""
.formatted(clientSessionTable, userSessionTable)));
}
private void generateUpdateQueryForPostgresSQL(String clientSessionTable, String userSessionTable) {
statements.add(new RawParameterizedSqlStatement("""
UPDATE %s cs
SET realm_id = us.realm_id
FROM %s us
WHERE cs.user_session_id = us.user_session_id AND cs.offline_flag = us.offline_flag AND cs.realm_id IS NULL"""
.formatted(clientSessionTable, userSessionTable)));
}
}

View File

@@ -133,6 +133,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
entity.setTimestamp(clientSession.getTimestamp());
entity.setData(model.getData());
entity.setRealmId(adapter.getRealm().getId());
if (!exists) {
em.persist(entity);
@@ -272,14 +273,9 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
@Override
public Map<String, Long> getUserSessionsCountsByClients(RealmModel realm, boolean offline) {
String offlineStr = offlineToString(offline);
TypedQuery<Object[]> query = em.createNamedQuery("findClientSessionsClientIds", Object[].class);
query.setParameter("offline", offlineStr);
query.setParameter("realmId", realm.getId());
query.setParameter("lastSessionRefresh", calculateOldestSessionTime(realm, offline));
TypedQuery<Object[]> query = em.createNamedQuery("findClientSessionsClientIds", Object[].class)
.setParameter("offline", offlineToString(offline))
.setParameter("realmId", realm.getId());
return closing(query.getResultStream())
.collect(Collectors.toMap(row -> {

View File

@@ -35,7 +35,7 @@ import org.hibernate.annotations.DynamicUpdate;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@NamedQueries({
@NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId = :realmId)"),
@NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.realmId = :realmId"),
@NamedQuery(name="deleteClientSessionsByClient", query="delete from PersistentClientSessionEntity sess where sess.clientId = :clientId and sess.clientId != 'external'"),
@NamedQuery(name="deleteClientSessionsByExternalClient", query="delete from PersistentClientSessionEntity sess where sess.clientStorageProvider = :clientStorageProvider and sess.externalClientId = :externalClientId and sess.clientStorageProvider != 'internal'"),
@NamedQuery(name="deleteClientSessionsByClientStorageProvider", query="delete from PersistentClientSessionEntity sess where sess.clientStorageProvider = :clientStorageProvider"),
@@ -44,14 +44,18 @@ import org.hibernate.annotations.DynamicUpdate;
@NamedQuery(name="deleteClientSessionsByUserSessions", query="delete from PersistentClientSessionEntity sess where sess.userSessionId in (:userSessionId) and sess.offline = :offline"),
// The query "deleteExpiredClientSessions" is deprecated (since 26.5) and may be removed in the future.
@NamedQuery(name="deleteExpiredClientSessions", query="delete from PersistentClientSessionEntity sess where sess.offline = :offline AND sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId = :realmId AND u.offline = :offline AND u.lastSessionRefresh < :lastSessionRefresh)"),
@NamedQuery(name="deleteClientSessionsByRealmSessionType", query="delete from PersistentClientSessionEntity sess where sess.offline = :offline AND sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId = :realmId and u.offline = :offline)"),
@NamedQuery(name="deleteClientSessionsByRealmSessionType", query="delete from PersistentClientSessionEntity sess where sess.offline = :offline AND sess.realmId = :realmId"),
@NamedQuery(name="findClientSessionsByUserSession", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline = :offline"),
@NamedQuery(name="findClientSessionsOrderedByIdInterval", query="select sess from PersistentClientSessionEntity sess where sess.offline = :offline and sess.userSessionId >= :fromSessionId and sess.userSessionId <= :toSessionId order by sess.userSessionId"),
@NamedQuery(name="findClientSessionsOrderedByIdExact", query="select sess from PersistentClientSessionEntity sess where sess.offline = :offline and sess.userSessionId IN (:userSessionIds)"),
@NamedQuery(name="findClientSessionsCountByClient", query="select count(sess) from PersistentClientSessionEntity sess where sess.offline = :offline and sess.clientId = :clientId and sess.clientId != 'external'"),
@NamedQuery(name="findClientSessionsCountByExternalClient", query="select count(sess) from PersistentClientSessionEntity sess where sess.offline = :offline and sess.clientStorageProvider = :clientStorageProvider and sess.externalClientId = :externalClientId and sess.clientStorageProvider != 'internal'"),
@NamedQuery(name="findClientSessionsByUserSessionAndClient", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline = :offline and sess.clientId=:clientId and sess.clientId != 'external'"),
@NamedQuery(name="findClientSessionsByUserSessionAndExternalClient", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline = :offline and sess.clientStorageProvider = :clientStorageProvider and sess.externalClientId = :externalClientId and sess.clientStorageProvider != 'internal'")
@NamedQuery(name="findClientSessionsByUserSessionAndExternalClient", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline = :offline and sess.clientStorageProvider = :clientStorageProvider and sess.externalClientId = :externalClientId and sess.clientStorageProvider != 'internal'"),
@NamedQuery(name="findClientSessionsClientIds", query="SELECT sess.clientId, sess.externalClientId, sess.clientStorageProvider, count(sess)" +
" FROM PersistentClientSessionEntity sess" +
" WHERE sess.offline = :offline AND sess.realmId = :realmId" +
" GROUP BY sess.clientId, sess.externalClientId, sess.clientStorageProvider"),
})
@Table(name="OFFLINE_CLIENT_SESSION")
@Entity
@@ -91,6 +95,9 @@ public class PersistentClientSessionEntity {
@Column(name="DATA")
protected String data;
@Column(name = "REALM_ID", length = 36)
protected String realmId;
public String getUserSessionId() {
return userSessionId;
}
@@ -147,6 +154,14 @@ public class PersistentClientSessionEntity {
this.data = data;
}
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public static class Key implements Serializable {
protected String userSessionId;

View File

@@ -63,10 +63,6 @@ import org.hibernate.annotations.DynamicUpdate;
@NamedQuery(name="findUserSessionsByExternalClientId", query="SELECT sess FROM PersistentUserSessionEntity sess INNER JOIN PersistentClientSessionEntity clientSess " +
" ON sess.userSessionId = clientSess.userSessionId AND clientSess.clientStorageProvider = :clientStorageProvider AND sess.offline = clientSess.offline AND clientSess.externalClientId = :externalClientId WHERE sess.offline = :offline " +
" AND sess.realmId = :realmId AND sess.lastSessionRefresh >= :lastSessionRefresh ORDER BY sess.userSessionId"),
@NamedQuery(name="findClientSessionsClientIds", query="SELECT clientSess.clientId, clientSess.externalClientId, clientSess.clientStorageProvider, count(clientSess)" +
" FROM PersistentClientSessionEntity clientSess INNER JOIN PersistentUserSessionEntity sess ON clientSess.userSessionId = sess.userSessionId AND sess.offline = clientSess.offline" +
" WHERE sess.offline = :offline AND sess.realmId = :realmId AND sess.lastSessionRefresh >= :lastSessionRefresh" +
" GROUP BY clientSess.clientId, clientSess.externalClientId, clientSess.clientStorageProvider"),
@NamedQuery(name = "findUserSessionAndDataWithNullRememberMeLastRefresh",
query = "SELECT sess.userSessionId, sess.userId, sess.data" +
" FROM PersistentUserSessionEntity sess" +

View File

@@ -55,4 +55,46 @@
</createIndex>
</changeSet>
<changeSet author="keycloak" id="26.6.0-44424-index-css-user-session-and-offline">
<preConditions onFail="MARK_RAN" onSqlOutput="TEST">
<not>
<indexExists tableName="OFFLINE_CLIENT_SESSION" indexName="IDX_OFFLINE_CSS_BY_USER_SESSION_AND_OFFLINE"/>
</not>
</preConditions>
<createIndex tableName="OFFLINE_CLIENT_SESSION" indexName="IDX_OFFLINE_CSS_BY_USER_SESSION_AND_OFFLINE">
<column name="OFFLINE_FLAG" type="VARCHAR(4)"/>
<column name="USER_SESSION_ID" type="VARCHAR(36)"/>
</createIndex>
</changeSet>
<changeSet author="keycloak" id="26.6.0-44424-create-realm-in-client-session">
<preConditions onFail="MARK_RAN" onSqlOutput="TEST">
<not>
<columnExists tableName="OFFLINE_CLIENT_SESSION" columnName="REALM_ID"/>
</not>
</preConditions>
<addColumn tableName="OFFLINE_CLIENT_SESSION">
<column name="REALM_ID" type="VARCHAR(36)"/>
</addColumn>
</changeSet>
<changeSet author="keycloak" id="26.6.0-44424-set-realm-in-client-session">
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate26_6_0_OfflineClientSessionRealm"/>
</changeSet>
<changeSet author="keycloak" id="26.6.0-44424-idx-css-realm-and-clients">
<preConditions onFail="MARK_RAN" onSqlOutput="TEST">
<not>
<indexExists tableName="OFFLINE_CLIENT_SESSION" indexName="IDX_OFFLINE_CSS_BY_CLIENT_AND_REALM"/>
</not>
</preConditions>
<createIndex tableName="OFFLINE_CLIENT_SESSION" indexName="IDX_OFFLINE_CSS_BY_CLIENT_AND_REALM">
<column name="REALM_ID" type="VARCHAR(36)"/>
<column name="OFFLINE_FLAG" type="VARCHAR(4)"/>
<column name="CLIENT_ID" type="VARCHAR(255)"/>
<column name="CLIENT_STORAGE_PROVIDER" type="VARCHAR(36)"/>
<column name="EXTERNAL_CLIENT_ID" type="VARCHAR(255)"/>
</createIndex>
</changeSet>
</databaseChangeLog>