This is an automated email from the ASF dual-hosted git repository.

andreapatricelli pushed a commit to branch 4_0_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/4_0_X by this push:
     new 8db44e4cf0 [SYNCOPE-1906] Fixing order by clause on plain attribute 
for mariadb and mysql (#1171)
8db44e4cf0 is described below

commit 8db44e4cf0a860b22a12f7927ed2a2f78f9f063a
Author: Andrea Patricelli <[email protected]>
AuthorDate: Mon Sep 8 10:51:28 2025 +0200

    [SYNCOPE-1906] Fixing order by clause on plain attribute for mariadb and 
mysql (#1171)
---
 .../jpa/dao/MariaDBJPAAnySearchDAO.java            |  30 ++++++
 .../persistence/jpa/dao/MySQLJPAAnySearchDAO.java  |  14 ++-
 .../core/persistence/jpa/inner/AnySearchTest.java  |  90 ++++++++++++++++++
 .../apache/syncope/fit/core/UserIssuesITCase.java  | 102 +++++++++++++++++++++
 4 files changed, 233 insertions(+), 3 deletions(-)

diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java
index 9bef72a4aa..148f52ac00 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java
@@ -38,6 +38,7 @@ import 
org.apache.syncope.core.persistence.api.entity.PlainAttr;
 import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.springframework.data.domain.Sort;
 
 public class MariaDBJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
 
@@ -68,6 +69,35 @@ public class MariaDBJPAAnySearchDAO extends 
AbstractJPAAnySearchDAO {
                 entityManager);
     }
 
+    @Override
+    protected void parseOrderByForPlainSchema(
+            final SearchSupport svs,
+            final OrderBySupport obs,
+            final OrderBySupport.Item item,
+            final Sort.Order clause,
+            final PlainSchema schema,
+            final String fieldName) {
+
+        // keep track of involvement of non-mandatory schemas in the order by 
clauses
+        obs.nonMandatorySchemas = 
!"true".equals(schema.getMandatoryCondition());
+
+        obs.views.add(svs.field());
+
+        item.select = new StringBuilder().
+                append("( SELECT 
usa").append('.').append(key(schema.getType())).
+                append(" FROM ").append(schema.isUniqueConstraint()
+                ? svs.asSearchViewSupport().uniqueAttr().name()
+                : svs.asSearchViewSupport().attr().name()).
+                append(" usa WHERE usa.any_id = ").
+                append(defaultSV(svs).alias()).
+                append(".any_id").
+                append(" AND usa.schema_id ='").append(fieldName).append("'").
+                append(" LIMIT 1").
+                append(") AS ").append(fieldName).toString();
+        item.where = "plainSchema = '" + fieldName + '\'';
+        item.orderBy = fieldName + ' ' + clause.getDirection().name();
+    }
+    
     @Override
     protected Pair<Boolean, AnySearchNode> getQuery(
             final AttrCond cond,
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java
index ca3b237fcb..4a5e497067 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java
@@ -84,9 +84,17 @@ public class MySQLJPAAnySearchDAO extends 
AbstractJPAAnySearchDAO {
 
         obs.views.add(svs.field());
 
-        item.select = svs.field().alias() + '.'
-                + (schema.isUniqueConstraint() ? "attrUniqueValue" : 
key(schema.getType()))
-                + " AS " + fieldName;
+        item.select = new StringBuilder().append(schema.isUniqueConstraint()
+                ? "( SELECT JSON_UNQUOTE(JSON_EXTRACT(usa.attrUniqueValue, '$. 
" + key(schema.getType()) + "')) "
+                : "( SELECT usa." + key(schema.getType())).
+                append(" FROM ").
+                append(svs.field().name()).
+                append(" usa WHERE usa.any_id = ").
+                append(svs.field().alias()).
+                append(".any_id").
+                append(" AND usa.plainSchema 
='").append(fieldName).append("'").
+                append(" LIMIT 1").
+                append(") AS ").append(fieldName).toString();
         item.where = "plainSchema = '" + fieldName + '\'';
         item.orderBy = fieldName + ' ' + clause.getDirection().name();
     }
diff --git 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
index e1dc24f772..b27d83fec6 100644
--- 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
+++ 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
@@ -36,10 +36,12 @@ import java.util.stream.Stream;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import 
org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.RoleDAO;
@@ -56,6 +58,7 @@ import 
org.apache.syncope.core.persistence.api.dao.search.RoleCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.apache.syncope.core.persistence.api.entity.PlainAttr;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AMembership;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
@@ -105,6 +108,12 @@ public class AnySearchTest extends AbstractTest {
     @Autowired
     private RoleDAO roleDAO;
 
+    @Autowired
+    private PlainSchemaDAO plainSchemaDAO;
+
+    @Autowired
+    private PlainAttrValidationManager validator;
+
     @BeforeEach
     public void adjustLoginDateForLocalSystem() throws ParseException {
         User rossini = userDAO.findByUsername("rossini").orElseThrow();
@@ -948,4 +957,85 @@ public class AnySearchTest extends AbstractTest {
         assertNotNull(users);
         assertEquals(4, users.size());
     }
+
+    @Test
+    public void issueSYNCOPE1906() {
+        User bellini = userDAO.findByUsername("bellini").orElseThrow();
+
+        PlainSchema ctypeSchema = 
plainSchemaDAO.findById("ctype").orElseThrow();
+        assertNotNull(ctypeSchema);
+
+        userDAO.save(addPlainAttr(bellini, ctypeSchema, "aa1"));
+
+        User puccini = userDAO.findByUsername("puccini").orElseThrow();
+        assertNotNull(bellini);
+
+        userDAO.save(addPlainAttr(puccini, ctypeSchema, "aa2"));
+
+        User verdi = userDAO.findByUsername("verdi").orElseThrow();
+        assertNotNull(verdi);
+
+        userDAO.save(addPlainAttr(verdi, ctypeSchema, "aa3"));
+
+        User vivaldi = userDAO.findByUsername("vivaldi").orElseThrow();
+        assertNotNull(vivaldi);
+
+        userDAO.save(addPlainAttr(vivaldi, ctypeSchema, "aa4"));
+
+        User rossini = userDAO.findByUsername("rossini").orElseThrow();
+        assertNotNull(rossini);
+
+        userDAO.save(addPlainAttr(rossini, ctypeSchema, "aa5"));
+
+        entityManager.flush();
+
+        AnyCond idCond = new AnyCond(AttrCond.Type.ISNOTNULL);
+        idCond.setSchema("id");
+
+        List<User> users =
+                searchDAO.search(SearchCond.of(idCond), List.of(new 
Sort.Order(Sort.Direction.DESC, "ctype")),
+                        AnyTypeKind.USER);
+
+        assertEquals("bellini", users.get(4).getUsername());
+        assertEquals("puccini", users.get(3).getUsername());
+        assertEquals("verdi", users.get(2).getUsername());
+        assertEquals("vivaldi", users.get(1).getUsername());
+        assertEquals("rossini", users.get(0).getUsername());
+
+        // order by ctype even not searching by it
+        AttrCond surnameCond = new AttrCond(AttrCond.Type.ILIKE);
+        surnameCond.setSchema("surname");
+        surnameCond.setExpression("%ini");
+
+        AttrCond coolCond = new AttrCond(AttrCond.Type.ISNULL);
+        coolCond.setSchema("cool");
+
+        users = searchDAO.search(SearchCond.and(SearchCond.of(surnameCond), 
SearchCond.of(coolCond)),
+                List.of(new Sort.Order(Sort.Direction.DESC, "ctype")), 
AnyTypeKind.USER);
+
+        assertFalse(users.isEmpty());
+        assertEquals(2, users.size());
+        
+        // order by unique attribute
+        users = searchDAO.search(SearchCond.of(idCond), List.of(new 
Sort.Order(Sort.Direction.DESC, "fullname")),
+                AnyTypeKind.USER);
+
+        assertEquals("vivaldi", users.get(4).getUsername());
+        assertEquals("puccini", users.get(3).getUsername());
+        assertEquals("rossini", users.get(2).getUsername());
+        assertEquals("verdi", users.get(1).getUsername());
+        assertEquals("bellini", users.get(0).getUsername());
+    }
+
+    private User addPlainAttr(final User user, final PlainSchema plainSchema, 
final String value) {
+        user.getPlainAttr(plainSchema.getKey())
+                .ifPresentOrElse(ctype -> 
ctype.getValues().get(0).setStringValue(value), () -> {
+                    PlainAttr attr = new PlainAttr();
+                    attr.setPlainSchema(plainSchema);
+                    attr.add(validator, value);
+
+                    user.add(attr);
+                });
+        return user;
+    }
 }
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
index e607048274..e10c4ac310 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
@@ -102,6 +102,7 @@ import org.apache.syncope.common.lib.types.StatusRType;
 import org.apache.syncope.common.lib.types.TaskType;
 import org.apache.syncope.common.lib.types.UnmatchingRule;
 import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.AnyQuery;
 import org.apache.syncope.common.rest.api.beans.RealmQuery;
 import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.syncope.common.rest.api.beans.TaskQuery;
@@ -1895,4 +1896,105 @@ public class UserIssuesITCase extends AbstractITCase {
             SCHEMA_SERVICE.delete(SchemaType.PLAIN, 
userWithDotSchema.getKey());
         }
     }
+
+    @Test
+    public void issueSYNCOPE1906() {
+        UserCR userCR = 
UserITCase.getUniqueSample("[email protected]");
+        userCR.getPlainAttrs()
+                .removeIf(attr -> "ctype".equals(attr.getSchema()) || 
"fullname".equals(attr.getSchema()));
+        userCR.getPlainAttrs().add(attr("ctype", "aa1"));
+        userCR.getPlainAttrs().add(attr("fullname", "aa1"));
+        UserTO aa1 = createUser(userCR).getEntity();
+
+        userCR = 
UserITCase.getUniqueSample("[email protected]");
+        userCR.getPlainAttrs()
+                .removeIf(attr -> "ctype".equals(attr.getSchema()) || 
"fullname".equals(attr.getSchema()));
+        userCR.getPlainAttrs().add(attr("ctype", "aa2"));
+        userCR.getPlainAttrs().add(attr("fullname", "aa2"));
+        UserTO aa2 = createUser(userCR).getEntity();
+
+        userCR = 
UserITCase.getUniqueSample("[email protected]");
+        userCR.getPlainAttrs()
+                .removeIf(attr -> "ctype".equals(attr.getSchema()) || 
"fullname".equals(attr.getSchema()));
+        userCR.getPlainAttrs().add(attr("ctype", "aa3"));
+        userCR.getPlainAttrs().add(attr("fullname", "aa3"));
+        UserTO aa3 = createUser(userCR).getEntity();
+
+        userCR = 
UserITCase.getUniqueSample("[email protected]");
+        userCR.getPlainAttrs()
+                .removeIf(attr -> "ctype".equals(attr.getSchema()) || 
"fullname".equals(attr.getSchema()));
+        userCR.getPlainAttrs().add(attr("ctype", "aa4"));
+        userCR.getPlainAttrs().add(attr("fullname", "aa4"));
+        UserTO aa4 = createUser(userCR).getEntity();
+
+        userCR = 
UserITCase.getUniqueSample("[email protected]");
+        userCR.getPlainAttrs()
+                .removeIf(attr -> "ctype".equals(attr.getSchema()) || 
"fullname".equals(attr.getSchema()));
+        userCR.getPlainAttrs().add(attr("ctype", "aa5"));
+        userCR.getPlainAttrs().add(attr("fullname", "aa5"));
+        UserTO aa5 = createUser(userCR).getEntity();
+
+        // add also other users with valued ctype
+        userCR = UserITCase.getUniqueSample("[email protected]");
+        userCR.getPlainAttrs()
+                .removeIf(attr -> "ctype".equals(attr.getSchema()) || 
"fullname".equals(attr.getSchema()));
+        userCR.getPlainAttrs().add(attr("ctype", "a ctype"));
+        userCR.getPlainAttrs().add(attr("fullname", "a fullname"));
+        createUser(userCR);
+
+        userCR = UserITCase.getUniqueSample("[email protected]");
+        userCR.getPlainAttrs()
+                .removeIf(attr -> "ctype".equals(attr.getSchema()) || 
"fullname".equals(attr.getSchema()));
+        userCR.getPlainAttrs().add(attr("ctype", "a ctype 2"));
+        userCR.getPlainAttrs().add(attr("fullname", "a fullname 2"));
+        createUser(userCR);
+        try {
+            await().until(() -> USER_SERVICE.search(new 
AnyQuery.Builder().fiql(
+                            
SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*")
+                                    .query())
+                    .size(0)
+                    .page(1)
+                    .build()).getTotalCount() == 5);
+            List<UserTO> users = USER_SERVICE.search(new 
AnyQuery.Builder().fiql(
+                            
SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*")
+                                    .query()).size(10).page(1).orderBy("ctype 
DESC").build()).getResult();
+
+            assertEquals(5, users.size());
+
+            assertEquals(aa1.getUsername(), users.get(4).getUsername());
+            assertEquals(aa2.getUsername(), users.get(3).getUsername());
+            assertEquals(aa3.getUsername(), users.get(2).getUsername());
+            assertEquals(aa4.getUsername(), users.get(1).getUsername());
+            assertEquals(aa5.getUsername(), users.get(0).getUsername());
+
+            users = USER_SERVICE.search(new AnyQuery.Builder().fiql(
+                            
SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*")
+                                    .query()).size(10).page(1).orderBy("ctype 
ASC").build()).getResult();
+
+            assertEquals(aa5.getUsername(), users.get(4).getUsername());
+            assertEquals(aa4.getUsername(), users.get(3).getUsername());
+            assertEquals(aa3.getUsername(), users.get(2).getUsername());
+            assertEquals(aa2.getUsername(), users.get(1).getUsername());
+            assertEquals(aa1.getUsername(), users.get(0).getUsername());
+
+            // order by unique attribute
+            users = USER_SERVICE.search(new AnyQuery.Builder().fiql(
+                            
SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*")
+                                    
.query()).size(10).page(1).orderBy("fullname ASC").build()).getResult();
+
+            assertEquals(aa5.getUsername(), users.get(4).getUsername());
+            assertEquals(aa4.getUsername(), users.get(3).getUsername());
+            assertEquals(aa3.getUsername(), users.get(2).getUsername());
+            assertEquals(aa2.getUsername(), users.get(1).getUsername());
+            assertEquals(aa1.getUsername(), users.get(0).getUsername());
+
+        } finally {
+            USER_SERVICE.search(new AnyQuery.Builder().fiql(
+                            
SyncopeClient.getUserSearchConditionBuilder().is("ctype").equalTo("aa*").query())
+                    .size(10)
+                    .page(1)
+                    .orderBy("ctype DESC")
+                    .build()).getResult().forEach(u -> deleteUser(u.getKey()));
+        }
+    }
 }

Reply via email to