Updating H2 database to 2.x

Closes #12607

Co-authored-by: Stian Thorgersen <stian@redhat.com>
This commit is contained in:
Alexander Schwartz 2022-10-07 11:38:00 +02:00 committed by Hynek Mlnařík
parent f49582cf63
commit 97c4495c4f
8 changed files with 88 additions and 25 deletions

View file

@ -34,7 +34,7 @@
<keycloak.connectionsJpa.database>keycloak</keycloak.connectionsJpa.database> <keycloak.connectionsJpa.database>keycloak</keycloak.connectionsJpa.database>
<keycloak.connectionsJpa.user>sa</keycloak.connectionsJpa.user> <keycloak.connectionsJpa.user>sa</keycloak.connectionsJpa.user>
<keycloak.connectionsJpa.password></keycloak.connectionsJpa.password> <keycloak.connectionsJpa.password></keycloak.connectionsJpa.password>
<keycloak.connectionsJpa.url>jdbc:h2:mem:test;MVCC=TRUE;DB_CLOSE_DELAY=-1</keycloak.connectionsJpa.url> <keycloak.connectionsJpa.url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</keycloak.connectionsJpa.url>
<jdbc.mvn.groupId>com.h2database</jdbc.mvn.groupId> <jdbc.mvn.groupId>com.h2database</jdbc.mvn.groupId>
<jdbc.mvn.artifactId>h2</jdbc.mvn.artifactId> <jdbc.mvn.artifactId>h2</jdbc.mvn.artifactId>
<jdbc.mvn.version>${h2.version}</jdbc.mvn.version> <jdbc.mvn.version>${h2.version}</jdbc.mvn.version>

View file

@ -175,8 +175,13 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
properties.put(AvailableSettings.JPA_NON_JTA_DATASOURCE, dataSource); properties.put(AvailableSettings.JPA_NON_JTA_DATASOURCE, dataSource);
} }
} else { } else {
properties.put(AvailableSettings.JPA_JDBC_URL, config.get("url")); String url = config.get("url");
properties.put(AvailableSettings.JPA_JDBC_DRIVER, config.get("driver")); String driver = config.get("driver");
if (driver.equals("org.h2.Driver")) {
url = addH2NonKeywords(url);
}
properties.put(AvailableSettings.JPA_JDBC_URL, url);
properties.put(AvailableSettings.JPA_JDBC_DRIVER, driver);
String user = config.get("user"); String user = config.get("user");
if (user != null) { if (user != null) {
@ -319,6 +324,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
return sql2012Dialect; return sql2012Dialect;
} }
} }
// For Oracle19c, we may need to set dialect explicitly to workaround https://hibernate.atlassian.net/browse/HHH-13184 // For Oracle19c, we may need to set dialect explicitly to workaround https://hibernate.atlassian.net/browse/HHH-13184
if (dbProductName.equals("Oracle") && connection.getMetaData().getDatabaseMajorVersion() > 12) { if (dbProductName.equals("Oracle") && connection.getMetaData().getDatabaseMajorVersion() > 12) {
logger.debugf("Manually specify dialect for Oracle to org.hibernate.dialect.Oracle12cDialect"); logger.debugf("Manually specify dialect for Oracle to org.hibernate.dialect.Oracle12cDialect");
@ -413,8 +419,13 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup);
return dataSource.getConnection(); return dataSource.getConnection();
} else { } else {
Class.forName(config.get("driver")); String url = config.get("url");
return DriverManager.getConnection(StringPropertyReplacer.replaceProperties(config.get("url"), System.getProperties()), config.get("user"), config.get("password")); String driver = config.get("driver");
if (driver.equals("org.h2.Driver")) {
url = addH2NonKeywords(url);
}
Class.forName(driver);
return DriverManager.getConnection(StringPropertyReplacer.replaceProperties(url, System.getProperties()), config.get("user"), config.get("password"));
} }
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to connect to database", e); throw new RuntimeException("Failed to connect to database", e);
@ -448,4 +459,23 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
private void migrateModel(KeycloakSession session) { private void migrateModel(KeycloakSession session) {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), MigrationModelManager::migrate); KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), MigrationModelManager::migrate);
} }
/**
* Starting with H2 version 2.x, marking "VALUE" as a non-keyword is necessary as some columns are named "VALUE" in the Keycloak schema.
* <p />
* Alternatives considered and rejected:
* <ul>
* <li>customizing H2 Database dialect -&gt; wouldn't work for existing Liquibase scripts.</li>
* <li>adding quotes to <code>@Column(name="VALUE")</code> annotations -&gt; would require testing for all DBs, wouldn't work for existing Liquibase scripts.</li>
* </ul>
* Downsides of this solution: Release notes needed to point out that any H2 JDBC URL parameter with <code>NON_KEYWORDS</code> needs to add the keyword <code>VALUE</code> manually.
* @return JDBC URL with <code>NON_KEYWORDS=VALUE</code> appended if the URL doesn't contain <code>NON_KEYWORDS=</code> yet
*/
private String addH2NonKeywords(String jdbcUrl) {
if (!jdbcUrl.contains("NON_KEYWORDS=")) {
jdbcUrl = jdbcUrl + ";NON_KEYWORDS=VALUE";
}
return jdbcUrl;
}
} }

View file

@ -276,17 +276,32 @@ public class JpaEventStoreProvider implements EventStoreProvider {
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<RealmAttributeEntity> cr = cb.createQuery(RealmAttributeEntity.class); CriteriaQuery<RealmAttributeEntity> cr = cb.createQuery(RealmAttributeEntity.class);
Root<RealmAttributeEntity> root = cr.from(RealmAttributeEntity.class); Root<RealmAttributeEntity> root = cr.from(RealmAttributeEntity.class);
cr.select(root).where(cb.and(cb.equal(root.get("name"),RealmAttributes.ADMIN_EVENTS_EXPIRATION),cb.greaterThan(root.get("value").as(Long.class),Long.valueOf(0)))); // unable to cast the CLOB to a BIGINT in the select for H2 2.x, therefore comparing strings only in the DB, and filtering again in the next statement
Map<Long, List<RealmAttributeEntity>> realms = em.createQuery(cr).getResultStream().collect(Collectors.groupingBy(attribute -> Long.valueOf(attribute.getValue()))); cr.select(root).where(cb.and(cb.equal(root.get("name"),RealmAttributes.ADMIN_EVENTS_EXPIRATION),cb.notEqual(root.get("value"), "0")));
Map<Long, List<RealmAttributeEntity>> realms = em.createQuery(cr).getResultStream()
// filtering again on the attribute as paring the CLOB to BIGINT didn't work in H2 2.x
.filter(attribute -> {
try {
return Long.parseLong(attribute.getValue()) > 0;
} catch (NumberFormatException ex) {
logger.warnf("Unable to parse value '%s' for attribute '%s' in realm '%s' (expecting it to be decimal numeric)",
attribute.getValue(),
RealmAttributes.ADMIN_EVENTS_EXPIRATION,
attribute.getRealm().getId(),
ex);
return false;
}
})
.collect(Collectors.groupingBy(attribute -> Long.valueOf(attribute.getValue())));
long current = Time.currentTimeMillis(); long current = Time.currentTimeMillis();
realms.entrySet().forEach(entry -> { realms.forEach((key, value) -> {
List<String> realmIds = entry.getValue().stream().map(RealmAttributeEntity::getRealm).map(RealmEntity::getId).collect(Collectors.toList()); List<String> realmIds = value.stream().map(RealmAttributeEntity::getRealm).map(RealmEntity::getId).collect(Collectors.toList());
int currentNumDeleted = em.createQuery("delete from AdminEventEntity where realmId in :realmIds and time < :eventTime") int currentNumDeleted = em.createQuery("delete from AdminEventEntity where realmId in :realmIds and time < :eventTime")
.setParameter("realmIds", realmIds) .setParameter("realmIds", realmIds)
.setParameter("eventTime", current - (Long.valueOf(entry.getKey()) * 1000)) .setParameter("eventTime", current - (key * 1000))
.executeUpdate(); .executeUpdate();
logger.tracef("Deleted %d admin events for the expiration %d", currentNumDeleted, entry.getKey()); logger.tracef("Deleted %d admin events for the expiration %d", currentNumDeleted, key);
}); });
} }
} }

View file

@ -88,10 +88,10 @@
<cxf.jaxrs.version>3.3.10</cxf.jaxrs.version> <cxf.jaxrs.version>3.3.10</cxf.jaxrs.version>
<cxf.undertow.version>3.3.10</cxf.undertow.version> <cxf.undertow.version>3.3.10</cxf.undertow.version>
<dom4j.version>2.1.3</dom4j.version> <dom4j.version>2.1.3</dom4j.version>
<h2.version>1.4.197</h2.version> <h2.version>2.1.214</h2.version>
<jakarta.persistence.version>2.2.3</jakarta.persistence.version> <jakarta.persistence.version>2.2.3</jakarta.persistence.version>
<hibernate.core.version>5.3.24.Final</hibernate.core.version> <hibernate.core.version>5.6.10.Final</hibernate.core.version>
<hibernate.c3p0.version>5.3.24.Final</hibernate.c3p0.version> <hibernate.c3p0.version>5.6.10.Final</hibernate.c3p0.version>
<infinispan.version>13.0.10.Final</infinispan.version> <infinispan.version>13.0.10.Final</infinispan.version>
<infinispan.protostream.processor.version>4.4.1.Final</infinispan.protostream.processor.version> <infinispan.protostream.processor.version>4.4.1.Final</infinispan.protostream.processor.version>
<javax.annotation-api.version>1.3.2</javax.annotation-api.version> <javax.annotation-api.version>1.3.2</javax.annotation-api.version>

View file

@ -107,11 +107,29 @@ public final class Database {
@Override @Override
public String apply(String alias) { public String apply(String alias) {
if ("dev-file".equalsIgnoreCase(alias)) { if ("dev-file".equalsIgnoreCase(alias)) {
return "jdbc:h2:file:${kc.home.dir:${kc.db-url-path:" + System.getProperty("user.home") + "}}" + File.separator + "${kc.data.dir:data}" return addH2NonKeywords("jdbc:h2:file:${kc.home.dir:${kc.db-url-path:" + System.getProperty("user.home") + "}}" + File.separator + "${kc.data.dir:data}"
+ File.separator + "h2" + File.separator + File.separator + "h2" + File.separator
+ "keycloakdb${kc.db-url-properties:;;AUTO_SERVER=TRUE}"; + "keycloakdb${kc.db-url-properties:;;AUTO_SERVER=TRUE}");
} }
return "jdbc:h2:mem:keycloakdb${kc.db-url-properties:}"; return addH2NonKeywords("jdbc:h2:mem:keycloakdb${kc.db-url-properties:}");
}
/**
* Starting with H2 version 2.x, marking "VALUE" as a non-keyword is necessary as some columns are named "VALUE" in the Keycloak schema.
* <p />
* Alternatives considered and rejected:
* <ul>
* <li>customizing H2 Database dialect -&gt; wouldn't work for existing Liquibase scripts.</li>
* <li>adding quotes to <code>@Column(name="VALUE")</code> annotations -&gt; would require testing for all DBs, wouldn't work for existing Liquibase scripts.</li>
* </ul>
* Downsides of this solution: Release notes needed to point out that any H2 JDBC URL parameter with <code>NON_KEYWORDS</code> needs to add the keyword <code>VALUE</code> manually.
* @return JDBC URL with <code>NON_KEYWORDS=VALUE</code> appended if the URL doesn't contain <code>NON_KEYWORDS=</code> yet
*/
private String addH2NonKeywords(String jdbcUrl) {
if (!jdbcUrl.contains("NON_KEYWORDS=")) {
jdbcUrl = jdbcUrl + ";NON_KEYWORDS=VALUE";
}
return jdbcUrl;
} }
}, },
asList("liquibase.database.core.H2Database"), asList("liquibase.database.core.H2Database"),

View file

@ -252,12 +252,12 @@ public class ConfigurationTest {
System.setProperty(CLI_ARGS, "--db=dev-file"); System.setProperty(CLI_ARGS, "--db=dev-file");
SmallRyeConfig config = createConfig(); SmallRyeConfig config = createConfig();
assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue()); assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());
assertEquals("jdbc:h2:file:" + System.getProperty("user.home") + File.separator + "data" + File.separator + "h2" + File.separator + "keycloakdb;;AUTO_SERVER=TRUE", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); assertEquals("jdbc:h2:file:" + System.getProperty("user.home") + File.separator + "data" + File.separator + "h2" + File.separator + "keycloakdb;;AUTO_SERVER=TRUE;NON_KEYWORDS=VALUE", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
System.setProperty(CLI_ARGS, "--db=dev-mem"); System.setProperty(CLI_ARGS, "--db=dev-mem");
config = createConfig(); config = createConfig();
assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue()); assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());
assertEquals("jdbc:h2:mem:keycloakdb", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); assertEquals("jdbc:h2:mem:keycloakdb;NON_KEYWORDS=VALUE", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
assertEquals("h2", config.getConfigValue("quarkus.datasource.db-kind").getValue()); assertEquals("h2", config.getConfigValue("quarkus.datasource.db-kind").getValue());
System.setProperty(CLI_ARGS, "--db=dev-mem" + ARG_SEPARATOR + "--db-username=other"); System.setProperty(CLI_ARGS, "--db=dev-mem" + ARG_SEPARATOR + "--db-username=other");
@ -320,13 +320,13 @@ public class ConfigurationTest {
System.setProperty(CLI_ARGS, "--db=dev-file"); System.setProperty(CLI_ARGS, "--db=dev-file");
SmallRyeConfig config = createConfig(); SmallRyeConfig config = createConfig();
assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue()); assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());
assertEquals("jdbc:h2:file:test-dir" + File.separator + "data" + File.separator + "h2" + File.separator + "keycloakdb;;test=test;test1=test1", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); assertEquals("jdbc:h2:file:test-dir" + File.separator + "data" + File.separator + "h2" + File.separator + "keycloakdb;;test=test;test1=test1;NON_KEYWORDS=VALUE", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
assertEquals("xa", config.getConfigValue("quarkus.datasource.jdbc.transactions").getValue()); assertEquals("xa", config.getConfigValue("quarkus.datasource.jdbc.transactions").getValue());
System.setProperty(CLI_ARGS, ""); System.setProperty(CLI_ARGS, "");
config = createConfig(); config = createConfig();
assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue()); assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());
assertEquals("jdbc:h2:file:test-dir" + File.separator + "data" + File.separator + "h2" + File.separator + "keycloakdb;;test=test;test1=test1", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); assertEquals("jdbc:h2:file:test-dir" + File.separator + "data" + File.separator + "h2" + File.separator + "keycloakdb;;test=test;test1=test1;NON_KEYWORDS=VALUE", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
System.setProperty("kc.db-url-properties", "?test=test&test1=test1"); System.setProperty("kc.db-url-properties", "?test=test&test1=test1");
System.setProperty(CLI_ARGS, "--db=mariadb"); System.setProperty(CLI_ARGS, "--db=mariadb");

View file

@ -436,7 +436,7 @@
<keycloak.connectionsJpa.database>keycloak</keycloak.connectionsJpa.database> <keycloak.connectionsJpa.database>keycloak</keycloak.connectionsJpa.database>
<keycloak.connectionsJpa.user>sa</keycloak.connectionsJpa.user> <keycloak.connectionsJpa.user>sa</keycloak.connectionsJpa.user>
<keycloak.connectionsJpa.password/> <keycloak.connectionsJpa.password/>
<keycloak.connectionsJpa.url>jdbc:h2:mem:test;MVCC=TRUE;DB_CLOSE_DELAY=-1</keycloak.connectionsJpa.url> <keycloak.connectionsJpa.url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</keycloak.connectionsJpa.url>
</properties> </properties>
</profile> </profile>
<profile> <profile>

View file

@ -19,7 +19,7 @@
<keycloak.connectionsJpa.database>keycloak</keycloak.connectionsJpa.database> <keycloak.connectionsJpa.database>keycloak</keycloak.connectionsJpa.database>
<keycloak.connectionsJpa.user>sa</keycloak.connectionsJpa.user> <keycloak.connectionsJpa.user>sa</keycloak.connectionsJpa.user>
<keycloak.connectionsJpa.password></keycloak.connectionsJpa.password> <keycloak.connectionsJpa.password></keycloak.connectionsJpa.password>
<keycloak.connectionsJpa.url>jdbc:h2:mem:test;MVCC=TRUE;DB_CLOSE_DELAY=-1</keycloak.connectionsJpa.url> <keycloak.connectionsJpa.url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</keycloak.connectionsJpa.url>
<jdbc.mvn.groupId>com.h2database</jdbc.mvn.groupId> <jdbc.mvn.groupId>com.h2database</jdbc.mvn.groupId>
<jdbc.mvn.artifactId>h2</jdbc.mvn.artifactId> <jdbc.mvn.artifactId>h2</jdbc.mvn.artifactId>
<jdbc.mvn.version>${h2.version}</jdbc.mvn.version> <jdbc.mvn.version>${h2.version}</jdbc.mvn.version>