hibernate/hibernate-orm · error · IllegalArgumentException

Can't emulate equality preserving row constructor through st

Error message

Can't emulate equality preserving row constructor through string concatenation for expression [%s] which is of type [%s]

What it means

Twin of the order-preserving case: on dialects without row value constructor syntax, Hibernate emulates tuple equality (a,b) = (c,d) by concatenating components into one string and comparing strings. The equality-preserving wrapper only handles STRING cast types (numeric falls through to the failure branch here when no padding rule exists). This IllegalArgumentException reports the expression and its cast type when no equality-preserving string encoding is known.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java:3411

			case OFFSET_TIMESTAMP:
			case ZONE_TIMESTAMP:
				if ( dialect.requiresCastForConcatenatingNonStrings() ) {
					return castToString( expression );
				}
				// Should we maybe always cast instead? Not sure what is faster/better...
				final BasicType<String> stringType = getStringType();
				return new SelfRenderingFunctionSqlAstExpression<>(
						"concat",
						findSelfRenderingFunction( "concat", 2 ),
						List.of(
								expression,
								new QueryLiteral<>( "", stringType )
						),
						stringType,
						stringType
				);
		}
		throw new IllegalArgumentException(
				String.format(
						"Can't emulate equality preserving row constructor through string concatenation for expression [%s] which is of type [%s]",
						expression,
						jdbcMapping.getCastType()
				)
		);
	}

	private int wrapRowComponentAsEqualityPreservingConcatArgumentSizeEstimate(Expression expression) {
		final JdbcMapping jdbcMapping = expression.getExpressionType().getSingleJdbcMapping();
		switch ( jdbcMapping.getCastType() ) {
			case STRING:
				if ( expression.getExpressionType() instanceof SqlTypedMapping sqlTypedMapping ) {
					final Long length = sqlTypedMapping.getLength();
					if ( length != null ) {
						return length.intValue();
					}
				}

View on GitHub (pinned to fad1729dce)

Solutions

  1. Split the tuple comparison into one comparison per component (where a = :a and b = :b)
  2. Cast components to string in the query so the STRING branch applies
  3. Switch to a database with native row value constructor support
  4. Upgrade Hibernate for broader cast-type coverage in the emulation

Example fix

// before
session.createQuery("from Order o where (o.id.customerId, o.id.seqNo) = (:cid, :seq)")...

// after
session.createQuery("from Order o where o.id.customerId = :cid and o.id.seqNo = :seq")...
Defensive patterns

Strategy: fallback

Validate before calling

boolean nativeRowCtor = dialect.supportsRowValueConstructorSyntax();
boolean eqEmulable = components.stream().allMatch(e ->
    e.getExpressionType().getSingleJdbcMapping().getCastType() == CastType.STRING);
if (!nativeRowCtor && !eqEmulable) { /* compare component-wise */ }

Try / catch

try {
    query.list();
} catch (IllegalArgumentException e) {
    if (e.getMessage() != null && e.getMessage().startsWith("Can't emulate equality preserving row constructor")) {
        // rewrite as component-wise equality
    } else throw e;
}

Prevention

When it happens

Trigger: An HQL/Criteria equality or comparison predicate over a tuple (where (a,b) = :pair, comparing an embeddable/id-class, (key(m), value(m)) comparisons) rendered on a dialect without native row constructor support, where a component's jdbcMapping.getCastType() is not handled by the wrapRowComponentAsEqualityPreservingConcatArgument switch.

Common situations: Filtering by composite id class on SQL Server or MySQL < 8.0.19; comparing embeddables in where clauses; @EmbeddedId entities used in Map keys with element comparisons; custom value types with non-standard CastType.

Related errors


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/bc9f79533dce32db. Report an issue: GitHub.