Add semrack index postgresql
All checks were successful
Maven build / build (push) Successful in 3m17s

This commit is contained in:
Guillaume Dugas
2025-11-19 23:31:18 +01:00
parent de339f9554
commit 03b1e0851b
62 changed files with 1301 additions and 318 deletions

View File

@@ -1,15 +0,0 @@
package fr.codeanddata.semrack.storage.postgres.dtos;
import lombok.*;
import java.util.List;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class InLookupParams {
String field;
List<String> values;
}

View File

@@ -1,23 +0,0 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import fr.codeanddata.semrack.core.services.SemrackLookupService;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Map;
@Identifier("and")
@ApplicationScoped
public class AndLookup implements SemrackJpaLookupExpression<List<Map<String, Object>>> {
@Inject
SemrackLookupService lookupService;
@Override
public String apply(Object expressions) {
final List<Map<String, Object>> typedExpressions = convert(expressions);
return String.join(" AND ", typedExpressions.stream().map(lookupService::lookup).toList());
}
}

View File

@@ -1,24 +0,0 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@Identifier("contains")
@ApplicationScoped
public class ContainsLookup implements SemrackJpaLookupExpression<Object> {
@Inject
ObjectMapper objectMapper;
@Override
public String apply(Object params) {
try {
return "document @> '" + objectMapper.writeValueAsString(params) + "'";
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -1,31 +0,0 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import fr.codeanddata.semrack.core.exceptions.SemrackRuntimeException;
import fr.codeanddata.semrack.storage.postgres.dtos.InLookupParams;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Identifier("in")
@ApplicationScoped
public class InLookup implements SemrackJpaLookupExpression<InLookupParams> {
@Override
public String apply(Object params) {
final InLookupParams inLookupParams = convert(params, InLookupParams.class);
final List<String> fields = new ArrayList<>(Arrays.asList(inLookupParams.getField().split("\\.")));
if (fields.isEmpty()) {
throw new SemrackRuntimeException("Field expected");
}
String query = "->> '" + fields.removeLast() + "'";
if (!fields.isEmpty()) {
query = "-> '" + String.join("' -> '", fields) + "' " + query;
}
return "document " + query + " in " + "('" + String.join("', '", inLookupParams.getValues()) + "')";
}
}

View File

@@ -1,23 +0,0 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import fr.codeanddata.semrack.core.services.SemrackLookupService;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Map;
@Identifier("or")
@ApplicationScoped
public class OrLookup implements SemrackJpaLookupExpression<List<Map<String, Object>>> {
@Inject
SemrackLookupService lookupService;
@Override
public String apply(Object expressions) {
final List<Map<String, Object>> typedExpressions = convert(expressions);
return String.join(" OR ", typedExpressions.stream().map(lookupService::lookup).toList());
}
}

View File

@@ -1,6 +0,0 @@
package fr.codeanddata.semrack.storage.postgres.operators;
import fr.codeanddata.semrack.core.SemrackLookupExpression;
public interface SemrackJpaLookupExpression<T> extends SemrackLookupExpression<T> {
}

View File

@@ -1,14 +1,10 @@
package fr.codeanddata.semrack.storage.postgres.storage;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.codeanddata.semrack.core.SemdocStorage;
import fr.codeanddata.semrack.core.enums.SemrackSortDirection;
import fr.codeanddata.semrack.core.models.*;
import fr.codeanddata.semrack.core.models.SemrackDocument;
import fr.codeanddata.semrack.core.services.SemrackLookupService;
import fr.codeanddata.semrack.core.utils.UIDGenerator;
import fr.codeanddata.semrack.storage.postgres.entities.SemrackDocumentEntity;
import fr.codeanddata.semrack.storage.postgres.utils.PathMapper;
import io.quarkus.hibernate.reactive.panache.PanacheRepository;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
@@ -16,7 +12,8 @@ import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.*;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
public class SemdocJpaStorage implements SemdocStorage, PanacheRepository<SemrackDocumentEntity> {
@@ -24,42 +21,21 @@ public class SemdocJpaStorage implements SemdocStorage, PanacheRepository<Semrac
@Inject
UIDGenerator generator;
@Inject
PathMapper pathMapper;
@Inject
SemrackLookupService lookupService;
@Inject
ObjectMapper objectMapper;
@Override
@WithSession
public Uni<SemrackDocument> readDocument(String uid) {
public Uni<SemrackDocument> get(String uid) {
return find("uid = ?1", uid)
.firstResult().map(d -> Optional.ofNullable(d).map(SemrackDocumentEntity::getDocument).orElse(null));
}
@Override
@WithSession
public Uni<SearchResult> searchDocument(SearchRequest query) {
return search(query);
}
@Override
public Uni<Long> countDocuments(SearchRequest request) {
final String lookup = lookupService.lookup(request.getFilter());
final String whereClause = lookup.isEmpty() ? "" : " WHERE " + lookup;
return getSession()
.chain(s -> s.createNativeQuery("SELECT count(id) FROM semrack_document" + whereClause, Long.class).getSingleResultOrNull())
.map(count -> count == null ? 0 : count);
}
@Override
public Uni<Boolean> documentsExist(SearchRequest query) {
return countDocuments(query)
.map(c -> c > 0);
public Uni<List<SemrackDocument>> get(List<String> uids) {
return find("uid in ?1", uids).list()
.map(d -> d.stream().map(SemrackDocumentEntity::getDocument).toList());
}
@WithTransaction
@@ -93,76 +69,76 @@ public class SemdocJpaStorage implements SemdocStorage, PanacheRepository<Semrac
.map(x -> document);
}
Uni<SearchResult> search(SearchRequest request) {
final String baseQuery = buildBaseQuery(request);
final StringBuilder sorting = new StringBuilder();
if (request.getSort() != null && !request.getSort().isEmpty()) {
sorting.append(" ORDER BY ");
sorting.append(String.join(", ", request.getSort().stream().map(sort -> sort.getField() + " " + Optional.ofNullable(sort.getDirection()).orElse(SemrackSortDirection.asc).name()).toList()));
}
final StringBuilder paginate = new StringBuilder();
if (request.getPaginate() != null) {
final SemrackPagination pagination = request.getPaginate();
final Integer size = pagination.getSize() == null ? 200 : pagination.getSize();
final Integer page = pagination.getPage() == null ? 0 : pagination.getPage();
paginate.append(" LIMIT ").append(size).append(" OFFSET ").append(size * page);
}
return getSession()
.chain(s -> {
final SearchResult searchResult = SearchResult.builder().build();
return countDocuments(request)
.invoke(count -> searchResult.setPagination(PaginationInfo.builder()
.page(Optional.ofNullable(request.getPaginate()).map(SemrackPagination::getPage).orElse(0))
.size(Optional.ofNullable(request.getPaginate()).map(SemrackPagination::getSize).orElse(0))
.total(count)
.build()))
.call(() -> s
.createNativeQuery(baseQuery + sorting + paginate, Object.class).getResultList()
.invoke(results -> searchResult.setDocuments(project(request.getFields(), results))))
.map(count -> searchResult);
});
}
String buildBaseQuery(SearchRequest request) {
final String lookup = lookupService.lookup(request.getFilter());
final String whereClause = lookup.isEmpty() ? "" : " WHERE " + lookup;
if (request.getFields() == null || request.getFields().isEmpty()) {
return "SELECT document FROM semrack_document" + whereClause;
} else {
return "SELECT " + toJsonbPathExtract(request.getFields()) + " FROM semrack_document" + whereClause;
}
}
String toJsonbPathExtract(List<String> fields) {
return String.join(",", fields.stream().map(field -> {
final String serializedField = "'" + String.join("','", field.split("\\.")) + "'";
return "jsonb_extract_path(document, " + serializedField + ")";
}).toList());
}
List<SemrackDocument> project(List<String> fields, Object results) {
final List<?> projectedResults = objectMapper.convertValue(results, List.class);
if (fields == null || fields.isEmpty()) {
return projectedResults.stream().map(result -> objectMapper.convertValue(result, SemrackDocument.class)).toList();
} else {
return projectedResults.stream().map(result -> {
final List<Object> row = fields.size() == 1 ? List.of(result) : objectMapper.convertValue(result, new TypeReference<>() {
});
final List<Object> rowMut = new ArrayList<>(row);
final List<String> fieldCp = new ArrayList<>(fields);
final Map<String, Object> document = new HashMap<>();
while (!fieldCp.isEmpty() && !row.isEmpty()) {
final String field = fieldCp.removeFirst();
final Object value = rowMut.removeFirst();
pathMapper.map(field, document, value);
}
return objectMapper.convertValue(document, SemrackDocument.class);
}).toList();
}
}
// Uni<SearchResult> search(SearchRequest request) {
// final String baseQuery = buildBaseQuery(request);
//
// final StringBuilder sorting = new StringBuilder();
// if (request.getSort() != null && !request.getSort().isEmpty()) {
// sorting.append(" ORDER BY ");
// sorting.append(String.join(", ", request.getSort().stream().map(sort -> sort.getField() + " " + Optional.ofNullable(sort.getDirection()).orElse(SemrackSortDirection.asc).name()).toList()));
// }
//
// final StringBuilder paginate = new StringBuilder();
// if (request.getPaginate() != null) {
// final SemrackPagination pagination = request.getPaginate();
// final Integer size = pagination.getSize() == null ? 200 : pagination.getSize();
// final Integer page = pagination.getPage() == null ? 0 : pagination.getPage();
//
// paginate.append(" LIMIT ").append(size).append(" OFFSET ").append(size * page);
// }
//
// return getSession()
// .chain(s -> {
// final SearchResult searchResult = SearchResult.builder().build();
// return countDocuments(request)
// .invoke(count -> searchResult.setPagination(PaginationInfo.builder()
// .page(Optional.ofNullable(request.getPaginate()).map(SemrackPagination::getPage).orElse(0))
// .size(Optional.ofNullable(request.getPaginate()).map(SemrackPagination::getSize).orElse(0))
// .total(count)
// .build()))
// .call(() -> s
// .createNativeQuery(baseQuery + sorting + paginate, Object.class).getResultList()
// .invoke(results -> searchResult.setDocuments(project(request.getFields(), results))))
// .map(count -> searchResult);
// });
// }
//
// String buildBaseQuery(SearchRequest request) {
// final String lookup = lookupService.lookup(request.getFilter());
// final String whereClause = lookup.isEmpty() ? "" : " WHERE " + lookup;
//
// if (request.getFields() == null || request.getFields().isEmpty()) {
// return "SELECT document FROM semrack_document" + whereClause;
// } else {
// return "SELECT " + toJsonbPathExtract(request.getFields()) + " FROM semrack_document" + whereClause;
// }
// }
//
// String toJsonbPathExtract(List<String> fields) {
// return String.join(",", fields.stream().map(field -> {
// final String serializedField = "'" + String.join("','", field.split("\\.")) + "'";
// return "jsonb_extract_path(document, " + serializedField + ")";
// }).toList());
// }
//
// List<SemrackDocument> project(List<String> fields, Object results) {
// final List<?> projectedResults = objectMapper.convertValue(results, List.class);
// if (fields == null || fields.isEmpty()) {
// return projectedResults.stream().map(result -> objectMapper.convertValue(result, SemrackDocument.class)).toList();
// } else {
// return projectedResults.stream().map(result -> {
// final List<Object> row = fields.size() == 1 ? List.of(result) : objectMapper.convertValue(result, new TypeReference<>() {
// });
// final List<Object> rowMut = new ArrayList<>(row);
// final List<String> fieldCp = new ArrayList<>(fields);
// final Map<String, Object> document = new HashMap<>();
// while (!fieldCp.isEmpty() && !row.isEmpty()) {
// final String field = fieldCp.removeFirst();
// final Object value = rowMut.removeFirst();
// pathMapper.map(field, document, value);
// }
// return objectMapper.convertValue(document, SemrackDocument.class);
// }).toList();
// }
// }
}

View File

@@ -2,6 +2,8 @@ quarkus.datasource.db-kind=postgresql
quarkus.flyway.enabled=true
quarkus.flyway.active=false
quarkus.flyway.migrate-at-start=true
quarkus.flyway.out-of-order=true
quarkus.flyway.ignore-missing-migrations=true
quarkus.native.resources.includes=src/main/resources/db/migration/**
quarkus.native.additional-build-args=-H:SerializationConfigurationResources=serialization-config.json