diff --git a/rsql-jpa/src/main/java/io/github/perplexhub/rsql/jsonb/JsonbSupport.java b/rsql-jpa/src/main/java/io/github/perplexhub/rsql/jsonb/JsonbSupport.java index bac49e1c..d9fea5fc 100644 --- a/rsql-jpa/src/main/java/io/github/perplexhub/rsql/jsonb/JsonbSupport.java +++ b/rsql-jpa/src/main/java/io/github/perplexhub/rsql/jsonb/JsonbSupport.java @@ -3,6 +3,7 @@ import static io.github.perplexhub.rsql.RSQLVisitorBase.getEntityManagerMap; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.EnumSet; import java.util.Map; @@ -19,13 +20,22 @@ import jakarta.persistence.criteria.Path; import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.ManagedType; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; import org.springframework.orm.jpa.vendor.Database; +import org.springframework.util.ClassUtils; /** * Support for jsonb expression. */ public class JsonbSupport { + /** + * is annotation present on classpath ? + */ + private static final boolean isHibernatePresent = ClassUtils.isPresent( + "org.hibernate.annotations.JdbcTypeCode", JsonbSupport.class.getClassLoader()); + private static final Set JSON_SUPPORT = EnumSet.of(Database.POSTGRESQL); private static final Map NEGATE_OPERATORS = @@ -99,15 +109,43 @@ public static boolean isJsonType(String mappedProperty, ManagedType classMeta * @return true if the attribute is a jsonb attribute */ private static boolean isJsonColumn(Attribute attribute) { + return isJsonbColumn(attribute) || isJdbcTypeCodeJSON(attribute); + } + + /** + * Returns whether the given attribute is a jsonb column. + * + * @param attribute the attribute + * @return true if the column is a jsonb column + */ + private static boolean isJsonbColumn(Attribute attribute) { + return getFieldAnnotation(attribute, Column.class) + .map(Column::columnDefinition) + .map(s -> s.toLowerCase().startsWith("jsonb")) + .orElse(false); + } + + /** + * Returns whether the given attribute is annotated with {@link JdbcTypeCode} and {@code value == SqlTypes.JSON}. + * + * @param attribute the attribute + * @return true if the column is a jsonb column + */ + private static boolean isJdbcTypeCodeJSON(Attribute attribute) { + return isHibernatePresent && getFieldAnnotation(attribute, JdbcTypeCode.class) + .map(JdbcTypeCode::value) + .map(code -> SqlTypes.JSON == code) + .orElse(false); + } + + private static Optional getFieldAnnotation(Attribute attribute, Class annotationClass) { return Optional.ofNullable(attribute) .filter(attr -> attr.getJavaMember() instanceof Field) .map(attr -> ((Field) attr.getJavaMember())) - .map(field -> field.getAnnotation(Column.class)) - .map(Column::columnDefinition) - .map("jsonb"::equalsIgnoreCase) - .orElse(false); + .map(field -> field.getAnnotation(annotationClass)); } + /** * Returns the database of the given attribute. * diff --git a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportPostgresJsonTest.java b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportPostgresJsonTest.java index 7d968634..fd6feb6f 100644 --- a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportPostgresJsonTest.java +++ b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportPostgresJsonTest.java @@ -1,15 +1,18 @@ package io.github.perplexhub.rsql; import io.github.perplexhub.rsql.jsonb.JsonbConfiguration; +import io.github.perplexhub.rsql.model.AnotherJsonbEntity; import io.github.perplexhub.rsql.model.EntityWithJsonb; import io.github.perplexhub.rsql.model.JsonbEntity; import io.github.perplexhub.rsql.model.PostgresJsonEntity; +import io.github.perplexhub.rsql.repository.jpa.postgres.AnotherJsonbEntityRepository; import io.github.perplexhub.rsql.repository.jpa.postgres.EntityWithJsonbRepository; import io.github.perplexhub.rsql.repository.jpa.postgres.JsonbEntityRepository; import io.github.perplexhub.rsql.repository.jpa.postgres.PostgresJsonEntityRepository; import jakarta.persistence.EntityManager; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -44,6 +47,9 @@ class RSQLJPASupportPostgresJsonTest { @Autowired private JsonbEntityRepository jsonbEntityRepository; + @Autowired + private AnotherJsonbEntityRepository anotherJsonbEntityRepository; + @BeforeEach void setup(@Autowired EntityManager em) { RSQLVisitorBase.setEntityManagerDatabase(Map.of(em, Database.POSTGRESQL)); @@ -737,4 +743,16 @@ void testJsonSearchCustomFunction(List entities, String rsql entities.forEach(e -> e.setId(null)); } + + @Test + void testAlternateJsonColumnDefinitions() { + anotherJsonbEntityRepository.saveAllAndFlush(List.of( + AnotherJsonbEntity.builder().id(UUID.randomUUID()).data("{\"a\":\"b\",\"c\":1}").other("{\"d\":\"e\"}").build(), + AnotherJsonbEntity.builder().id(UUID.randomUUID()).data("{\"a\":\"q\",\"c\":2}").other("{\"d\":\"h\"}").build() + )); + assertThat(anotherJsonbEntityRepository.findAll(toSpecification("data.a==b"))).hasSize(1); + assertThat(anotherJsonbEntityRepository.findAll(toSpecification("other.d==h"))).hasSize(1); + assertThat(anotherJsonbEntityRepository.findAll(toSpecification("generated.a==b"))).hasSize(1); + assertThat(anotherJsonbEntityRepository.findAll(toSpecification("formula.c==1"))).hasSize(1);; + } } diff --git a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/model/AnotherJsonbEntity.java b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/model/AnotherJsonbEntity.java new file mode 100644 index 00000000..2b4e326c --- /dev/null +++ b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/model/AnotherJsonbEntity.java @@ -0,0 +1,41 @@ +package io.github.perplexhub.rsql.model; + +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.*; +import org.hibernate.annotations.Formula; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.annotations.Type; +import org.hibernate.type.SqlTypes; + +import java.util.UUID; + +@Getter +@Setter +@EqualsAndHashCode(of = "id") +@ToString +@Entity +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class AnotherJsonbEntity { + + @Id + private UUID id; + + @JdbcTypeCode(SqlTypes.JSON) + private String data; + + @Type(JsonType.class) + @Column(columnDefinition = "jsonb") + private String other; + + @Column(columnDefinition = "jsonb generated always as (data) stored", insertable = false, updatable = false) + private String generated; + + @JdbcTypeCode(SqlTypes.JSON) + @Formula("jsonb_set(data, '{f}','\"r\"', true)") + private String formula; +} diff --git a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/repository/jpa/postgres/AnotherJsonbEntityRepository.java b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/repository/jpa/postgres/AnotherJsonbEntityRepository.java new file mode 100644 index 00000000..cd90203c --- /dev/null +++ b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/repository/jpa/postgres/AnotherJsonbEntityRepository.java @@ -0,0 +1,11 @@ +package io.github.perplexhub.rsql.repository.jpa.postgres; + +import io.github.perplexhub.rsql.model.AnotherJsonbEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.UUID; + +public interface AnotherJsonbEntityRepository extends JpaRepository, + JpaSpecificationExecutor { +} \ No newline at end of file