hibernate/hibernate-orm · error · SemanticException

User-defined version types not supported for increment optio

Error message

User-defined version types not supported for increment option

What it means

For 'UPDATE VERSIONED', Hibernate increments the version column arithmetically in SQL (version = version + 1) when the version is numeric. If the version type is a UserVersionType (a custom user-supplied version strategy), Hibernate cannot derive a SQL increment expression for it, so addVersionedAssignment throws SemanticException('User-defined version types not supported for increment option'). The entity is versioned, but its version is user-defined, which blocks the bulk increment path.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/query/sqm/sql/spi/BaseSqmToSqlAstConverter.java:916

		finally {
			popProcessingStateStack();
			this.currentSqmStatement = oldSqmStatement;
			this.cteContainer = oldCteContainer;
		}
	}

	public void addVersionedAssignment(Consumer<Assignment> assignmentConsumer, SqmUpdateStatement<?> sqmStatement) {
		if ( sqmStatement.isVersioned() ) {
			final var persister =
					getMappingMetamodel()
							.findEntityDescriptor( sqmStatement.getTarget().getEntityName() );
			if ( !persister.isVersioned() ) {
				throw new SemanticException( "Increment option specified for update of non-versioned entity" );
			}

			final var versionType = persister.getVersionType();
			if ( versionType instanceof UserVersionType ) {
				throw new SemanticException( "User-defined version types not supported for increment option" );
			}

			currentClauseStack.push( Clause.SET );
			final var versionMapping = persister.getVersionMapping();
			final var targetColumnReferences = BasicValuedPathInterpretation.from(
					(SqmBasicValuedSimplePath<?>)
							SqmExpressionHelper.get( sqmStatement.getRoot(),
									versionMapping.getPartName() ),
					this,
					jpaQueryComplianceEnabled
			).getColumnReferences();
			currentClauseStack.pop();
			assert targetColumnReferences.size() == 1;

			final var versionColumn = targetColumnReferences.get( 0 );
			final var value =
					versionMapping.getJdbcMapping().getJdbcType().isTemporal()
							? new VersionTypeSeedParameterSpecification( versionMapping )

View on GitHub (pinned to fad1729dce)

Solutions

  1. Switch the version to a built-in incrementable type: @Version private long version; or @Version private Instant/LocalDateTime/... timestamp
  2. Remove VERSIONED from the bulk update and manage the custom version yourself: set version = :newVersion and filter where version = :oldVersion in the same statement
  3. Implement the increment through a custom SQL update (session.createNativeMutationQuery) where you control the version expression
  4. If the custom type merely wraps a number, replace it with the plain numeric field and convert at the boundaries

Example fix

-- before
@Entity class Doc { @Version MyStringVersion v; } // UserVersionType
UPDATE VERSIONED Doc d SET d.title = :t
-- after
@Entity class Doc { @Version long v; }
UPDATE VERSIONED Doc d SET d.title = :t
Defensive patterns

Strategy: validation

Validate before calling

// Reject VERSIONED updates when the version type is user-defined
EntityType<?> et = em.getMetamodel().entity(Doc.class);
SingularAttribute<?, ?> ver = et.getSingularAttributes().stream()
        .filter(SingularAttribute::isVersion).findFirst().orElseThrow();
// if a custom UserType backs it, require the non-VERSIONED form

Type guard

static boolean builtInVersion(EntityManager em, Class<?> c) {
    Class<?> jt = em.getMetamodel().entity(c).getSingularAttributes().stream()
        .filter(a -> a.isVersion()).findFirst().map(a -> a.getJavaType()).orElse(null);
    return Number.class.isAssignableFrom(jt) || java.util.Date.class.isAssignableFrom(jt)
        || java.time.temporal.Temporal.class.isAssignableFrom(jt);
}

Try / catch

catch (SemanticException e) { if (e.getMessage().contains("User-defined version")) { /* drop VERSIONED and increment manually */ } else throw e; }

Prevention

When it happens

Trigger: Entity mapped with @Version on a field whose type is resolved through a custom UserType implementing UserVersionType (e.g. a legacy string GUID-based version, custom timestamp), then executing 'UPDATE VERSIONED ...'; reusing a legacy Hibernate 5 custom version type after upgrading to 6/7 where addVersionedAssignment was reworked to SQL-side increments.

Common situations: Legacy applications with custom version strategies (homegrown OptimisticLockStyle types); migrating from Hibernate 5 where versioned bulk updates with user types were handled differently; teams adding the new standardized VERSIONED keyword (JPA 3.2) to entities that predate built-in version types.

Related errors


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