{"record":{"id":"10811fcf062f2923","repo":"hibernate/hibernate-orm","slug":"multivalued-paths-are-only-allowed-for-the-member","errorCode":null,"errorMessage":"Multivalued paths are only allowed for the 'member of' operator","messagePattern":"Multivalued paths are only allowed for the 'member of' operator","errorType":"exception","errorClass":"SemanticException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java","lineNumber":433,"sourceCode":"\t}\n\n\t/**\n\t * @see TypecheckUtil#assertAssignable(String, SqmPath, SqmTypedNode, BindingContext)\n\t */\n\tpublic static void assertComparable(Expression<?> x, Expression<?> y, BindingContext bindingContext) {\n\t\tfinal var left = (SqmExpression<?>) x;\n\t\tfinal var right = (SqmExpression<?>) y;\n\t\tfinal Integer leftTupleLength = left.getTupleLength();\n\t\tfinal Integer rightTupleLength = right.getTupleLength();\n\t\tif ( leftTupleLength != null && rightTupleLength != null\n\t\t\t\t&& leftTupleLength.intValue() != rightTupleLength.intValue() ) {\n\t\t\tthrow new SemanticException( \"Cannot compare tuples of different lengths\" );\n\t\t}\n\n\t\t// SqmMemberOfPredicate is the only one allowing multivalued paths, its comparability is now evaluated in areTypesComparable\n\t\t// i.e. without calling this method, so we can check this here for other Predicates that do call this\n\t\tif ( left instanceof SqmPluralValuedSimplePath || right instanceof SqmPluralValuedSimplePath ) {\n\t\t\tthrow new SemanticException( \"Multivalued paths are only allowed for the 'member of' operator\" );\n\t\t}\n\n\t\t// allow comparing literal null to things\n\t\tif ( !( left instanceof SqmLiteralNull ) && !( right instanceof SqmLiteralNull ) ) {\n\t\t\tfinal var leftType = left.getExpressible();\n\t\t\tfinal var rightType = right.getExpressible();\n\t\t\tif ( leftType != null && rightType != null\n\t\t\t\t\t&& left.isEnum() && right.isEnum() ) {\n\t\t\t\t// this is needed by Hibernate Processor due to the weird\n\t\t\t\t// handling of enumerated types in the annotation processor\n\t\t\t\tif ( !Objects.equals( leftType.getTypeName(), rightType.getTypeName() ) ) {\n\t\t\t\t\tString.format(\n\t\t\t\t\t\t\t\"Cannot compare left expression of enumerated type '%s' with right expression of enumerated type '%s'\",\n\t\t\t\t\t\t\tleftType.getTypeName(),\n\t\t\t\t\t\t\trightType.getTypeName()\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}","sourceCodeStart":415,"sourceCodeEnd":451,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java#L415-L451","documentation":"Thrown as SemanticException by TypecheckUtil.assertComparable when either side of a comparison is an SqmPluralValuedSimplePath — a path to a plural attribute (a collection such as @OneToMany/@ManyToMany/@ElementCollection). Collections are multivalued and cannot be compared with =, <, like scalars; only the 'member of' predicate (and the internal comparability check used by it) accepts multivalued paths.","triggerScenarios":"HQL 'where p.tags = :tags' or 'where p.tags = someLiteral' where tags is a collection attribute; comparing a @OneToMany/@ElementCollection path with =, <>, <, >; criteria comparisons built on a collection path (cb.equal(root.get(\"tags\"), value)); forgetting that the collection itself is not the same as its elements or its size.","commonSituations":"Trying to filter 'entities whose collection equals X' by analogy with scalar fields; porting SQL that compares a join result set; new entity mappings adding @ElementCollection and reusing old scalar predicates; criteria predicates generated from generic filter specs that treat every attribute as singular.","solutions":["Use membership: 'where :tag member of p.tags' (HQL) or cb.isMember(tag, root.get(\"tags\")) (Criteria)","Compare sizes: 'where size(p.tags) = 2' or cb.size(...)/cb.count in a subquery","Compare a specific element: 'where p.tags[0] = :x' for indexed collections or join the collection and filter the alias","For 'collection equals set' semantics, compare ids of members via a subquery on elements"],"exampleFix":"// before\nString hql = \"from Person p where p.nicknames = 'beagle'\";\n// after\nString hql = \"from Person p where 'beagle' member of p.nicknames\";","handlingStrategy":"try-catch","validationCode":"// if the attribute is plural, force member-of semantics instead of equality\nstatic boolean isPlural(ManagedType<?> type, String attr) {\n    return type.getAttribute(attr).isCollection();\n}\n// build: cb.isMember(value, root.get(attr)) instead of cb.equal(root.get(attr), value)","typeGuard":"static boolean isSingularAttributePath(jakarta.persistence.metamodel.Attribute<?, ?> attr) {\n    return !attr.isCollection() && attr.getPersistentAttributeType() != Attribute.PersistentAttributeType.ENTITY;\n}","tryCatchPattern":"try {\n    return session.createQuery(hql, Person.class).getResultList();\n} catch (SemanticException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"Multivalued paths\")) {\n        // rewrite equality on a collection into a member-of predicate\n        String fixed = hql.replace(\"p.nicknames = :v\", \":v member of p.nicknames\");\n        return session.createQuery(fixed, Person.class).getResultList();\n    }\n    throw e;\n}","preventionTips":["Never compare collection paths with =; decide between member of, size(), or joined element filters","Check attr.isCollection() in generic filter builders before emitting cb.equal","Model 'collection equals' queries as subqueries on element ids"],"tags":["hibernate","hql","collections","member-of","semantic-analysis"],"backgroundTag":"collection-path-in-comparison","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}