This is an automated email from the ASF dual-hosted git repository.
chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new 73cb2e9dae [FIX] PGSQL: always init HSTORE correctly
73cb2e9dae is described below
commit 73cb2e9daef2626095fb41aad2a9409f8eb03f14
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Sep 14 10:49:36 2026 +0200
[FIX] PGSQL: always init HSTORE correctly
---
.../backends/postgres/PostgresTableManager.java | 18 ++-
.../PostgresTableManagerHstoreExtensionTest.java | 135 +++++++++++++++++++++
.../james/modules/data/PostgresCommonModule.java | 3 +-
3 files changed, 151 insertions(+), 5 deletions(-)
diff --git
a/backends-common/postgres/src/main/java/org/apache/james/backends/postgres/PostgresTableManager.java
b/backends-common/postgres/src/main/java/org/apache/james/backends/postgres/PostgresTableManager.java
index a3140dfa91..1e2fc5f533 100644
---
a/backends-common/postgres/src/main/java/org/apache/james/backends/postgres/PostgresTableManager.java
+++
b/backends-common/postgres/src/main/java/org/apache/james/backends/postgres/PostgresTableManager.java
@@ -20,6 +20,7 @@
package org.apache.james.backends.postgres;
import java.util.List;
+import java.util.function.Supplier;
import jakarta.inject.Inject;
@@ -28,12 +29,14 @@ import org.apache.james.lifecycle.api.Startable;
import org.jooq.DSLContext;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
+import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import io.r2dbc.spi.Connection;
+import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Result;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -42,14 +45,21 @@ public class PostgresTableManager implements Startable {
public static final int INITIALIZATION_PRIORITY = 1;
private static final Logger LOGGER =
LoggerFactory.getLogger(PostgresTableManager.class);
private final PostgresExecutor postgresExecutor;
+ private final Supplier<Publisher<? extends Connection>>
extensionConnectionSupplier;
private final PostgresDataDefinition module;
private final RowLevelSecurity rowLevelSecurity;
+ /**
+ * @param connectionFactory the non pooled factory the pool of {@code
postgresExecutor} is backed by. The hstore
+ * extension is created with it, see {@link
#initializePostgresExtension()}.
+ */
@Inject
public PostgresTableManager(PostgresExecutor postgresExecutor,
+ ConnectionFactory connectionFactory,
PostgresDataDefinition module,
PostgresConfiguration postgresConfiguration) {
this.postgresExecutor = postgresExecutor;
+ this.extensionConnectionSupplier = connectionFactory::create;
this.module = module;
this.rowLevelSecurity = postgresConfiguration.getRowLevelSecurity();
}
@@ -57,6 +67,7 @@ public class PostgresTableManager implements Startable {
@VisibleForTesting
public PostgresTableManager(PostgresExecutor postgresExecutor,
PostgresDataDefinition module, RowLevelSecurity rowLevelSecurity) {
this.postgresExecutor = postgresExecutor;
+ this.extensionConnectionSupplier = () ->
postgresExecutor.connectionFactory().getConnection();
this.module = module;
this.rowLevelSecurity = rowLevelSecurity;
}
@@ -69,13 +80,12 @@ public class PostgresTableManager implements Startable {
}
public Mono<Void> initializePostgresExtension() {
- return
Mono.usingWhen(postgresExecutor.connectionFactory().getConnection(),
- connection -> Mono.just(connection)
- .flatMapMany(pgConnection ->
pgConnection.createStatement("CREATE EXTENSION IF NOT EXISTS hstore")
+ return Mono.usingWhen(extensionConnectionSupplier.get(), // Not
pooled: r2dbc only registers the hstore codec on connections opened after the
extension exists
+ connection -> Flux.from(connection.createStatement("CREATE
EXTENSION IF NOT EXISTS hstore")
.execute())
.flatMap(Result::getRowsUpdated)
.then(),
- connection ->
postgresExecutor.connectionFactory().closeConnection(connection));
+ Connection::close);
}
public Mono<Void> initializeTables() {
diff --git
a/backends-common/postgres/src/test/java/org/apache/james/backends/postgres/PostgresTableManagerHstoreExtensionTest.java
b/backends-common/postgres/src/test/java/org/apache/james/backends/postgres/PostgresTableManagerHstoreExtensionTest.java
new file mode 100644
index 0000000000..c56e767df4
--- /dev/null
+++
b/backends-common/postgres/src/test/java/org/apache/james/backends/postgres/PostgresTableManagerHstoreExtensionTest.java
@@ -0,0 +1,135 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you 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.apache.james.backends.postgres;
+
+import static
org.apache.james.backends.postgres.PostgresFixture.Database.DEFAULT_DATABASE;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import
org.apache.james.backends.postgres.utils.PoolBackedPostgresConnectionFactory;
+import org.apache.james.backends.postgres.utils.PostgresExecutor;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.jooq.Field;
+import org.jooq.SQLDialect;
+import org.jooq.Table;
+import org.jooq.impl.DSL;
+import org.jooq.postgres.extensions.types.Hstore;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
+import io.r2dbc.postgresql.PostgresqlConnectionFactory;
+import io.r2dbc.spi.Connection;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+class PostgresTableManagerHstoreExtensionTest {
+ private static final int POOL_SIZE = 3;
+ private static final Table<?> TABLE = DSL.table("hstore_table");
+ private static final Field<Hstore> HSTORE_COLUMN =
DSL.field("hstore_column", PostgresCommons.DataTypes.HSTORE);
+ private static final PostgresDataDefinition MODULE =
PostgresDataDefinition.table(PostgresTable.name(TABLE.getName())
+ .createTableStep((dsl, tableName) ->
dsl.createTableIfNotExists(tableName)
+ .column(HSTORE_COLUMN))
+ .disableRowLevelSecurity()
+ .build());
+
+ private final PostgreSQLContainer<?> container =
DockerPostgresSingleton.SINGLETON;
+ private String databaseName;
+ private PoolBackedPostgresConnectionFactory pool;
+ private PostgresExecutor postgresExecutor;
+ private PostgresqlConnectionFactory connectionFactory;
+ private PostgresConfiguration postgresConfiguration;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ // A database of its own: the one of PostgresExtension already has the
hstore extension
+ databaseName = "hstore_" + UUID.randomUUID().toString().replace("-",
"");
+ container.execInContainer("psql", "-U", DEFAULT_DATABASE.dbUser(),
"-c", "CREATE DATABASE " + databaseName + ";");
+
+ postgresConfiguration = PostgresConfiguration.builder()
+ .databaseName(databaseName)
+ .databaseSchema(DEFAULT_DATABASE.schema())
+ .host(container.getHost())
+ .port(container.getMappedPort(PostgresFixture.PORT))
+ .username(DEFAULT_DATABASE.dbUser())
+ .password(DEFAULT_DATABASE.dbPassword())
+ .rowLevelSecurityEnabled(false)
+ .jooqReactiveTimeout(Optional.of(Duration.ofSeconds(20L)))
+ .build();
+ connectionFactory = new
PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder()
+ .host(postgresConfiguration.getHost())
+ .port(postgresConfiguration.getPort())
+ .database(databaseName)
+ .schema(DEFAULT_DATABASE.schema())
+ .username(DEFAULT_DATABASE.dbUser())
+ .password(DEFAULT_DATABASE.dbPassword())
+ .build());
+ pool = new
PoolBackedPostgresConnectionFactory(RowLevelSecurity.DISABLED, POOL_SIZE,
POOL_SIZE, connectionFactory);
+ postgresExecutor = new PostgresExecutor.Factory(pool,
postgresConfiguration, new RecordingMetricFactory()).create();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ pool.close().block();
+ container.execInContainer("psql", "-U", DEFAULT_DATABASE.dbUser(),
"-c", "DROP DATABASE IF EXISTS " + databaseName + " WITH (FORCE);");
+ }
+
+ @Test
+ void
everyPooledConnectionShouldDecodeHstoreWhenInitializingAFreshDatabase() {
+ new PostgresTableManager(postgresExecutor, connectionFactory, MODULE,
postgresConfiguration)
+ .initPostgres();
+
+ Map<String, String> entries = Map.of("[email protected]", "lr",
"[email protected]", "aeiklprstwx");
+ postgresExecutor.executeVoid(dsl -> Mono.from(dsl.insertInto(TABLE,
HSTORE_COLUMN)
+ .values(Hstore.hstore(entries))))
+ .block();
+
+ assertThat(readHstoreOnEveryPooledConnection())
+ .hasSize(POOL_SIZE)
+ .allSatisfy(read -> assertThat(read).isEqualTo(entries));
+ }
+
+ private List<Map<?, ?>> readHstoreOnEveryPooledConnection() {
+ return Flux.range(0, POOL_SIZE)
+ .flatMap(any -> pool.getConnection())
+ .collectList()
+ .flatMapMany(connections -> Flux.fromIterable(connections)
+ .concatMap(this::readHstore)
+ .concatWith(Flux.fromIterable(connections)
+ .concatMap(connection -> pool.closeConnection(connection))
+ .then(Mono.empty())))
+ .collectList()
+ .block();
+ }
+
+ private Mono<Map<?, ?>> readHstore(Connection connection) {
+ // An untyped select, as the DAOs do (selectFrom(TABLE_NAME) then
record.get(field, LinkedHashMap.class))
+ return Mono.from(DSL.using(connection,
SQLDialect.POSTGRES).selectFrom(TABLE))
+ .map(record -> record.get(HSTORE_COLUMN, LinkedHashMap.class));
+ }
+}
diff --git
a/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresCommonModule.java
b/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresCommonModule.java
index 05fa938f92..d8216a0352 100644
---
a/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresCommonModule.java
+++
b/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresCommonModule.java
@@ -137,9 +137,10 @@ public class PostgresCommonModule extends AbstractModule {
@Provides
@Singleton
PostgresTableManager postgresTableManager(PostgresExecutor
postgresExecutor,
+ ConnectionFactory
connectionFactory,
PostgresDataDefinition
postgresDataDefinition,
PostgresConfiguration
postgresConfiguration) {
- return new PostgresTableManager(postgresExecutor,
postgresDataDefinition, postgresConfiguration);
+ return new PostgresTableManager(postgresExecutor, connectionFactory,
postgresDataDefinition, postgresConfiguration);
}
@Provides
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]