diff --git a/src/main/java/io/r2dbc/postgresql/codec/PostgresTypes.java b/src/main/java/io/r2dbc/postgresql/codec/PostgresTypes.java index e33c8bfb..62571fdc 100644 --- a/src/main/java/io/r2dbc/postgresql/codec/PostgresTypes.java +++ b/src/main/java/io/r2dbc/postgresql/codec/PostgresTypes.java @@ -21,6 +21,9 @@ import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import io.r2dbc.spi.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.annotation.Nullable; @@ -40,10 +43,11 @@ public class PostgresTypes { public final static int NO_SUCH_TYPE = -1; // parameterized with %s for the comparator (=, IN), %s for the actual criteria value and %s for a potential LIMIT 1 statement - private static final String SELECT_PG_TYPE = "SELECT pg_type.* " + // language=sql + private static final String SELECT_PG_TYPE = "SELECT sp.schema_name, pg_type.* " + " FROM pg_catalog.pg_type " + " LEFT " - + " JOIN (select ns.oid as nspoid, ns.nspname, r.r " + + " JOIN (select ns.oid as nspoid, ns.nspname as schema_name, r.r " + " from pg_namespace as ns " + " join ( select s.r, (current_schemas(false))[s.r] as nspname " + " from generate_series(1, array_upper(current_schemas(false), 1)) as s(r) ) as r " @@ -66,12 +70,14 @@ public static PostgresTypes from(PostgresqlConnection connection) { } /** - * Lookup Postgres types by {@code typname}. Please note that {@code typname} inlined to use simple statements. Therefore, {@code typname} gets verified against {@link #TYPENAME} to prevent SQL + * Lookup Postgres types by {@code typname}. Please note that {@code typeName} inlined to use simple statements. Therefore, {@code typname} gets verified against {@link #TYPENAME} to prevent SQL * injection. * * @param typeName the type name. Must comply with the pattern {@code [a-zA-Z0-9_]+} * @return a mono emitting the {@link PostgresType} if found or {@link Mono#empty()} if not found + * @deprecated in favor of {@link #lookupTypes(String...)} */ + @Deprecated public Mono lookupType(String typeName) { if (!TYPENAME.matcher(Assert.requireNonNull(typeName, "typeName must not be null")).matches()) { throw new IllegalArgumentException(String.format("Invalid typename %s", typeName)); @@ -81,6 +87,17 @@ public Mono lookupType(String typeName) { .flatMap(it -> it.map(PostgresTypes::createType)).singleOrEmpty(); } + /** + * Lookup Postgres types by {@code typname}. Please note that {@code typeName} inlined to use simple statements. Therefore, {@code typeName} gets verified against {@link #TYPENAME} to prevent SQL + * injection. + * + * @param typeName the type name. Must comply with the pattern {@code [a-zA-Z0-9_]+} + * @return a mono emitting the {@link PostgresType} if found or {@link Mono#empty()} if not found + */ + public Flux lookupTypes(String... typeName) { + return lookupTypes(Arrays.asList(typeName)); + } + public Flux lookupTypes(Iterable typeNames) { StringJoiner joiner = new StringJoiner(",", "(", ")"); @@ -108,11 +125,12 @@ private static PostgresType createType(Row row, RowMetadata rowMetadata) { Long oid = row.get("oid", Long.class); String typname = row.get("typname", String.class); String typcategory = row.get("typcategory", String.class); + String schemaName = row.get("schema_name", String.class); Long typarrayOid = rowMetadata.contains("typarray") ? row.get("typarray", Long.class) : null; long unsignedTyparray = typarrayOid != null ? typarrayOid : NO_SUCH_TYPE; int typarray = typarrayOid != null ? PostgresqlObjectId.toInt(typarrayOid) : NO_SUCH_TYPE; - return new PostgresType(PostgresqlObjectId.toInt(oid), oid, typarray, unsignedTyparray, typname, typcategory); + return new PostgresType(PostgresqlObjectId.toInt(oid), oid, typarray, unsignedTyparray, typname, typcategory, schemaName); } public static class PostgresType implements Type, PostgresTypeIdentifier { @@ -129,9 +147,18 @@ public static class PostgresType implements Type, PostgresTypeIdentifier { private final String category; + /** + * The name of the schema where pg_type is stored. + */ + private final String schemaName; + @Nullable private final PostgresqlObjectId objectId; + /** + * @deprecated in favor of {@link #PostgresType(int, long, int, long, String, String, String)} + */ + @Deprecated public PostgresType(int oid, long unsignedOid, int typarray, long unsignedTyparray, String name, String category) { this.oid = oid; this.unsignedOid = unsignedOid; @@ -140,6 +167,18 @@ public PostgresType(int oid, long unsignedOid, int typarray, long unsignedTyparr this.name = name; this.category = category; this.objectId = PostgresqlObjectId.isValid(oid) ? PostgresqlObjectId.valueOf(oid) : null; + this.schemaName = ""; + } + + public PostgresType(int oid, long unsignedOid, int typarray, long unsignedTyparray, String name, String category, String schemaName) { + this.oid = oid; + this.unsignedOid = unsignedOid; + this.typarray = typarray; + this.unsignedTyparray = unsignedTyparray; + this.name = name; + this.category = category; + this.objectId = PostgresqlObjectId.isValid(oid) ? PostgresqlObjectId.valueOf(oid) : null; + this.schemaName = schemaName; } @Override @@ -160,7 +199,7 @@ public PostgresType asArrayType() { if (this.typarray > 0) { - return new PostgresType(this.typarray, this.unsignedTyparray, this.typarray, this.unsignedTyparray, this.name, this.category); + return new PostgresType(this.typarray, this.unsignedTyparray, this.typarray, this.unsignedTyparray, this.name, this.category, this.schemaName); } throw new IllegalStateException("No array type available for " + this); @@ -191,6 +230,13 @@ public String getName() { return this.name; } + /** + * @return The name of the schema where pg_type is stored. + */ + public String getSchemaName() { + return schemaName; + } + /** * @return {@code true} if the type is an array type (category code {@code A}) */ diff --git a/src/test/java/io/r2dbc/postgresql/codec/EnumCodecIntegrationTests.java b/src/test/java/io/r2dbc/postgresql/codec/EnumCodecIntegrationTests.java index 64c5ba4a..ee765457 100644 --- a/src/test/java/io/r2dbc/postgresql/codec/EnumCodecIntegrationTests.java +++ b/src/test/java/io/r2dbc/postgresql/codec/EnumCodecIntegrationTests.java @@ -97,7 +97,7 @@ void shouldBindEnumTypeAsString() { SERVER.getJdbcOperations().execute("CREATE TABLE enum_test (the_value my_enum_with_codec);"); PostgresTypes types = PostgresTypes.from(this.connection); - PostgresTypes.PostgresType type = types.lookupType("my_enum_with_codec").block(); + PostgresTypes.PostgresType type = types.lookupTypes("my_enum_with_codec").blockFirst(); this.connection.createStatement("INSERT INTO enum_test VALUES($1)") .bind("$1", Parameters.in(type, "HELLO")) @@ -118,7 +118,7 @@ void shouldBindEnumArrayTypeAsString() { SERVER.getJdbcOperations().execute("CREATE TABLE enum_test (the_value my_enum_with_codec[]);"); PostgresTypes types = PostgresTypes.from(this.connection); - PostgresTypes.PostgresType type = types.lookupType("my_enum_with_codec").block().asArrayType(); + PostgresTypes.PostgresType type = types.lookupTypes("my_enum_with_codec").blockFirst().asArrayType(); this.connection.createStatement("INSERT INTO enum_test VALUES($1)") .bind("$1", Parameters.in(type, new String[]{"HELLO", "WORLD"})) diff --git a/src/test/java/io/r2dbc/postgresql/codec/EnumCodecUnitTests.java b/src/test/java/io/r2dbc/postgresql/codec/EnumCodecUnitTests.java index 800d849c..a06be4b5 100644 --- a/src/test/java/io/r2dbc/postgresql/codec/EnumCodecUnitTests.java +++ b/src/test/java/io/r2dbc/postgresql/codec/EnumCodecUnitTests.java @@ -101,6 +101,7 @@ void shouldRegisterCodecAsFirst() { .identified("typarray", Long.class, 0L) .identified("typname", String.class, "foo") .identified("typcategory", String.class, "E") + .identified("schema_name", String.class, "public") .build()) .build()) .build(); @@ -130,6 +131,7 @@ void shouldRegisterCodecWithoutTyparray() { .identified("oid", Long.class, 42L) .identified("typname", String.class, "foo") .identified("typcategory", String.class, "E") + .identified("schema_name", String.class, "public") .build()) .build()) .build(); @@ -161,12 +163,14 @@ void shouldRegisterCodecAsLast() { .identified("typarray", Long.class, 0L) .identified("typname", String.class, "foo") .identified("typcategory", String.class, "E") + .identified("schema_name", String.class, "public") .build()) .row(MockRow.builder() .identified("oid", Long.class, 43L) .identified("typarray", Long.class, 0L) .identified("typname", String.class, "bar") .identified("typcategory", String.class, "E") + .identified("schema_name", String.class, "public") .build()) .build()) .build(); diff --git a/src/test/java/io/r2dbc/postgresql/codec/PostgresTypesIntegrationTests.java b/src/test/java/io/r2dbc/postgresql/codec/PostgresTypesIntegrationTests.java index 31a98d7a..e3f38bd3 100644 --- a/src/test/java/io/r2dbc/postgresql/codec/PostgresTypesIntegrationTests.java +++ b/src/test/java/io/r2dbc/postgresql/codec/PostgresTypesIntegrationTests.java @@ -19,6 +19,7 @@ import io.r2dbc.postgresql.AbstractIntegrationTests; import io.r2dbc.spi.Connection; import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcOperations; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -35,10 +36,42 @@ class PostgresTypesIntegrationTests extends AbstractIntegrationTests { @Test void shouldLookupSingleType() { - Mono.usingWhen(getConnectionFactory().create(), c -> { - return PostgresTypes.from(c).lookupType("varchar"); - }, Connection::close).map(PostgresTypes.PostgresType::getName).map(String::toLowerCase) - .as(StepVerifier::create).expectNext("varchar").verifyComplete(); + Flux + .usingWhen( // + getConnectionFactory().create(), // + c -> PostgresTypes.from(c).lookupTypes("varchar"), // + Connection::close // + ) + .map(PostgresTypes.PostgresType::getName) + .map(String::toLowerCase) + .as(StepVerifier::create) + .expectNext("varchar") + .verifyComplete(); + } + + @Test + void shouldLookupTypesInDifferentSchemas() { + + // test enum type set up + JdbcOperations jdbcOperations = SERVER.getJdbcOperations(); + jdbcOperations.execute("CREATE SCHEMA test_schema_1;"); + jdbcOperations.execute("CREATE SCHEMA test_schema_2;"); + jdbcOperations.execute("CREATE TYPE test_schema_1.test_enum AS ENUM ('FIRST', 'SECOND');"); + jdbcOperations.execute("CREATE TYPE test_schema_2.test_enum AS ENUM ('FIRST', 'SECOND');"); + + Flux + .usingWhen( // + getConnectionFactory().create(), // + c -> c // + .createStatement("SET SEARCH_PATH TO test_schema_1, test_schema_2;") // + .execute() // + .flatMap(unused -> PostgresTypes.from(c).lookupTypes("test_enum")), // + Connection::close // + ) + .as(StepVerifier::create) + .expectNextMatches(type -> type.getName().equals("test_enum")) + .expectNextMatches(type -> type.getName().equals("test_enum")) + .verifyComplete(); } @Test