This guide is for authors who publish a SQL Dialect independently of Hibernate ORM. The Dialect guide catalogs available Dialects; this guide explains how to implement, package, and verify one. Read the SPI and Service Provider Guide first for the meaning of USE, IMPLEMENT, and SUPPLY.

Provider project setup

Compile against the normal org.hibernate.orm:hibernate-core artifact. Do not depend on hibernate-testing or Hibernate’s test classes. Keep the provider in its own artifact and publish an explicit Hibernate compatibility range.

For Gradle builds, apply the provider tooling plugin alongside the Java plugin:

plugins {
    id("java-library")
    id("org.hibernate.orm.dialect-provider") version "{hibernate-version}"
}

dependencies {
    compileOnly("org.hibernate.orm:hibernate-core:{hibernate-version}")
}

hibernateDialectProvider {
    providerPackages.add("com.acme.hibernate.dialect")
    contractProfiles.add("com.acme.hibernate.dialect.AcmeDialectContractProfile")
}

The plugin resolves the classification metadata for the Hibernate X.Y release family, validates the normal provider jar, creates the dialectProviderTest source set, aligns the exact-version test kit with Core, and generates the small JUnit bridge for the explicitly named profile. Put provider-authored tests in src/dialectProviderTest/java.

The Gradle plugin is convenience tooling, not part of the Dialect SPI. Maven, Ant, and other builds should depend directly on the exact-version org.hibernate.orm:hibernate-dialect-testkit, configure JUnit Jupiter, and write the small @TestFactory bridge shown below. Static external-provider boundary validation is Gradle-only in this release. The test kit never scans for or discovers profiles.

Dialect construction and selection

Extend org.hibernate.dialect.Dialect or a base class explicitly classified for IMPLEMENT. A no-argument constructor should select the provider’s minimum supported database version. A metadata-aware constructor should copy or default the DialectResolutionInfo version before inherited registration begins.

Dialect#getVersion() is the immutable version captured during construction. Keep minimum-version resolution construction-safe: it must not read subclass fields before the superclass constructor has completed.

Applications may select a Dialect explicitly with hibernate.dialect.

Explicit names and automatic resolution

Prefer automatic resolution when the database and driver metadata reliably identify the provider’s database. Implement DialectResolver, return the provider Dialect for recognized DialectResolutionInfo, and return null to let the next resolver try. Register the implementation as a Java service:

public final class ExampleDialectResolver implements DialectResolver {
	@Override
	public Dialect resolveDialect(DialectResolutionInfo info) {
		return "ExampleDB".equals( info.getDatabaseName() ) ? new ExampleDialect() : null;
	}
}
META-INF/services/org.hibernate.engine.jdbc.dialect.spi.DialectResolver
org.example.orm.dialect.ExampleDialectResolver

Applications may instead list resolver class names in hibernate.dialect_resolvers. Explicitly configured resolvers run in their configured order, then service-loaded resolvers run in discovery order, and Hibernate’s standard resolver runs last. The first non-null Dialect wins. An ordinary resolver exception is logged and resolution continues, while a JDBC connection exception propagates.

Use DialectSelector only when the provider offers an explicit short name for hibernate.dialect. It maps the configured text, unchanged, to a Dialect class without constructing a Dialect. Return null for any unrecognized name so the next selector and then Hibernate’s standard selector may try it:

public final class ExampleDialectSelector implements DialectSelector {
	@Override
	public Class<? extends Dialect> resolve(String name) {
		return "Example".equals( name ) ? ExampleDialect.class : null;
	}
}
META-INF/services/org.hibernate.boot.registry.selector.spi.DialectSelector
org.example.orm.dialect.ExampleDialectSelector

Choose provider short names which are unlikely to collide. The first matching service wins, but Java service-provider ordering across independent artifacts is not a portable conflict-resolution mechanism. The test kit deliberately does not discover resolvers or selectors; a DialectContractProfile creates the exact Dialect under test.

Implement only the public DialectResolver or DialectSelector contract. org.hibernate.boot.registry.selector.internal.LazyServiceResolver is an internal Core adapter with no release-family compatibility guarantee and must not appear in provider code or signatures.

Strategy-oriented architecture

The Dialect is the provider’s composition root. It captures the database version and resolved configuration, and supplies focused collaborators to Hibernate’s type, query, JDBC, and schema subsystems. A strategy describes one coherent database variation; it is not a smaller name for an arbitrary group of Dialect methods.

Dialect as composition root

Prefer contracts under org.hibernate.dialect.<feature>.spi to copying Hibernate implementation code into a provider. Hibernate implementations in a matching .internal package are not provider contracts, even when their Java visibility permits an import.

Read both sides of a strategy relationship:

  • the strategy type states whether providers may use or implement it;

  • the Dialect method states how the strategy is supplied;

  • reciprocal @see links connect the two declarations;

  • IMPLEMENT does not imply SUPPLY, and SUPPLY does not imply discovery.

A supplied object should normally be stable for the Dialect’s lifetime and immutable or thread-safe. A shared constant is appropriate for behavior which does not depend on database version or provider configuration. Use a per-Dialect instance when the strategy captures resolved state. A Dialect may implement a strategy directly and return this when the strategy contract permits it.

Do not generalize the non-null rule. Some supply points deliberately return null to select Hibernate’s standard behavior or to report that a native form is unavailable. Follow the exact contract of each method.

Choosing an extension level

Use the narrowest supported extension which expresses the database behavior:

  1. Return a named standard constant or immutable profile.

  2. Construct a profile with its supported builder or factory.

  3. Compose or implement the focused strategy.

  4. Override a narrowly classified rendering hook when its contract explicitly supports provider implementation.

  5. Supply a custom SQL AST translator only when the variation changes SQL tree structure or rendering control flow in a way the focused strategies cannot express.

Do not replace a translator to change a single capability, and do not override adjacent strategies merely because one consumer happens to use them together. Independent capability answers allow Hibernate to compose the same database rules in schema tooling, mutation planning, SQL AST translation, and tests.

Family and implementation bases

A concrete selectable Dialect is not automatically a supported superclass. Extend Dialect or a family base whose Javadoc declares IMPLEMENT. For example, a Sybase-derived provider uses AbstractSybaseDialect, while SybaseDialect remains the concrete maintained Dialect selected by applications:

public final class ExampleSybaseDialect extends AbstractSybaseDialect {
	/// Creates the fixture Dialect for its declared database version.
	///
	/// @since 8.0
	public ExampleSybaseDialect() {
		super( DatabaseVersion.make( 17 ) );
	}

	/// Supplies the fixture-owned locking profile.
	///
	/// @since 8.0
	@Override
	public LockingSupport getLockingSupport() {
		return ExampleLockingSupport.INSTANCE;
	}
}

The family base preserves the maintained implementation and exposes the audited Dialect override surface. Choose the version constructor when the provider already knows its database version and the resolution-info constructor when bootstrap metadata determines it. Do not infer an implementation contract from package placement or Java visibility alone.

Identifier, metadata, and literal strategies

See the identifier SPI, literal SPI, and JDBC strategy SPI for the complete contracts.

Concern Primary contract Dialect supply point Provider guidance

Identifier rendering and limits

IdentifierSupport

getIdentifierSupport()

Describe quoting and identifier-length rules independently from JDBC metadata.

Identifier normalization

IdentifierHelperBuildRequest

buildIdentifierHelper(…​)

Refine the supplied builder and use JdbcMetadata availability explicitly.

Keyword behavior

KeywordSupport, KeywordRegistration

getKeywordSupport(), contributeKeywords(…​)

Use the strategy for keyword policy and the callback for registration lifecycle.

Literal rendering

LiteralSupport

getLiteralSupport()

Implement only database-specific literal forms and delegate the remainder.

Metadata correction

JdbcMetadataOverrides

getJdbcMetadataOverrides()

Override unreliable driver answers without pretending that metadata was read.

Parameter limits and markers

ParameterLimits, ParameterMarkerStrategy

getParameterLimits(), getNativeParameterMarkerStrategy()

Keep driver-sensitive marker selection consistent with the configured driver.

String-value semantics

StringValueSemantics

getStringValueSemantics()

Describe empty-string, trailing-space, and comparison behavior as one profile.

Result-column aliases

ColumnAliasExtractor

getColumnAliasExtractor()

Choose name- or label-based extraction according to reliable driver behavior.

Decorating identifier helpers

Prefer configuring the IdentifierHelperBuilder carried by IdentifierHelperBuildRequest. It already represents global quoting, automatic keyword quoting, identifier casing, namespace support, and common automatic-quoting rules. The inherited buildIdentifierHelper() initializes identifier casing, keywords, and namespace support from the request. Invoke it before replacing any of those values with authoritative provider settings, then build the final helper from the same builder. Settings which the inherited implementation does not initialize may be applied before invoking it.

When builder configuration cannot express one database rule, decorate the configured result with DelegatingIdentifierHelper. Do not implement IdentifierHelper directly. The interface is a USE + SUPPLY runtime result, while the delegating base is the supported IMPLEMENT contract and shields a provider from newly added forwarding operations.

Each forwarding operation is independent. If custom normalization must affect the three toIdentifier() entry points, override those entry points as well as normalizeQuoting(). Call super for ordinary forwarding and retain only the returned helper as the decorator’s delegate; do not retain the build request, builder, or metadata view.

This provider-owned decorator quotes an otherwise legal identifier beginning with provider_:

public final class ExampleIdentifierHelper extends DelegatingIdentifierHelper {
	public ExampleIdentifierHelper(IdentifierHelper delegate) {
		super( delegate );
	}

	@Override
	public Identifier normalizeQuoting(Identifier identifier) {
		final Identifier normalized = super.normalizeQuoting( identifier );
		return normalized != null
				&& !normalized.isQuoted()
				&& normalized.getText().startsWith( "provider_" )
				? normalized.quoted()
				: normalized;
	}

	@Override
	public Identifier toIdentifier(String text) {
		return normalizeQuoting( Identifier.toIdentifier( text ) );
	}

	@Override
	public Identifier toIdentifier(String text, boolean quoted) {
		return normalizeQuoting( Identifier.toIdentifier( text, quoted ) );
	}

	@Override
	public Identifier toIdentifier(String text, boolean quoted, boolean isExplicit) {
		return normalizeQuoting( Identifier.toIdentifier( text, quoted, false, isExplicit ) );
	}
}

The Dialect applies automatic-quoting settings not initialized by the inherited implementation and wraps its result:

public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuildRequest request) {
	request.builder().setAutoQuoteInitialUnderscore( true );
	if ( !request.jdbcMetadata().isJdbcMetadataAccessible() ) {
		request.builder().setAutoQuoteDollar( true );
	}
	return new ExampleIdentifierHelper( super.buildIdentifierHelper( request ) );
}

Composing datetime literals

LiteralSupport owns the complete SQL representation of date, time, and timestamp literals. A provider implementation is therefore responsible for database-specific introducers, delimiters, casts, and escape syntax. The StandardDateTimeLiteralRendering utility supplies just the standard date or time value between those database-specific elements.

For example, a database which requires its own timestamp introducer but accepts the standard value representation may compose the two concerns:

@Override
public void appendDateTimeLiteral(
        SqlAppender appender,
        TemporalAccessor temporalAccessor,
        TemporalType precision,
        TimeZone jdbcTimeZone) {
    appender.appendSql( "vendor_timestamp '" );
    StandardDateTimeLiteralRendering.appendAsTimestampWithMicros(
            appender,
            temporalAccessor,
            getTemporalValueSemantics().supportsLiteralOffset(),
            jdbcTimeZone,
            ZeroOffsetLiteralStyle.NUMERIC_OFFSET
    );
    appender.appendSql( '\'' );
}

Choose the zero-offset spelling explicitly: UTC_DESIGNATOR emits Z, while NUMERIC_OFFSET emits +00:00. Nonzero offsets retain their numeric value. Use the overload matching the database’s fractional-second precision, and use the local-time variants only when the literal syntax intentionally excludes an offset. Providers should not import the internal DateTimeUtils; the public utility is the supported composition boundary.

JDBC metadata is authoritative only when it is accessible and trustworthy. Metadata-free bootstrap must obtain every required fallback from the Dialect. Keyword contribution is therefore a lifecycle operation, while identifier and metadata support objects are stable strategies.

The aggregate-column, locking, row-level-security, and temporal-table extension families are incubating. Their contracts may change within an X.Y release family and are excluded from the corresponding SPI compatibility guarantee. Check the generated Javadoc for @Incubating before depending on a strategy, request, result, or SQL AST contract.

The central SQL AST and translator contracts, translator factories and requests, JdbcParameterFactory, and JdbcOperations builders are no longer incubating. The specialized SqlAstTranslator.renderNamedSetReturningFunction() hook and SetReturningFunctionType, together with the SQM-coupled QueryTransformer, remain incubating. This does not stabilize the separate execution, mapping-model mutation, or result-processing contracts; their individual lifecycle markers continue to apply.

Type and temporal strategies

See the type SPI, temporal-type SPI, temporal-table SPI, array SPI, and LOB SPI for the complete contracts.

Concern Primary contract Dialect supply point Provider guidance

Physical type capacities

TypeSizingProfile

getTypeSizingProfile()

Report supported capacities and precision defaults; use UNSUPPORTED only as documented.

Runtime size resolution

SizeStrategy

getSizeStrategy()

Customize mapped-size calculation without changing the physical capability profile.

Direct Java Time JDBC access

DirectJavaTimeJdbcSupport

getDirectJavaTimeJdbcSupport()

Report exact Java classes accepted for scalar access and independently inside native JDBC STRUCT and ARRAY containers.

Enum declarations and checks

EnumSupport

getEnumSupport()

Keep enum DDL, casts, declarations, and value checks in one strategy; distinguish textual and ordinal DDL when their relational representations differ.

Array types and literals

ArraySupport

getArraySupport()

Describe native array behavior; separate it from aggregate-column support.

LOB behavior

LobSupport

getLobSupport()

Coordinate LOB type classification, materialization, binding, and merge behavior.

Untyped null binding

ObjectNullBindingStrategy

getObjectNullBindingStrategy()

Choose the complete binding policy instead of independent Boolean answers.

Nationalized and zoned types

NationalizationSupport, TimeZoneSupport

getNationalizationSupport(), getTimeZoneSupport()

Report type-system behavior, not temporal literal syntax.

Current date and time

CurrentTemporalSupport

getCurrentTemporalSupport()

Supply current temporal expressions and database-side timestamp retrieval; report whether the timestamp expression uses standard current_timestamp syntax.

Temporal formatting

TemporalFormatSupport

getTemporalFormatSupport()

Translate format patterns without absorbing arithmetic or value semantics.

Temporal arithmetic

TemporalOperationSupport

getTemporalOperationSupport()

Own timestamp add/diff patterns and other operation grammar.

Temporal values

TemporalValueSemantics

getTemporalValueSemantics()

Describe precision loss, rounding, and offset preservation.

System-versioned tables

TemporalTableSupport

getTemporalTableSupport()

Keep temporal-table DDL and historical-query behavior separate from temporal types.

Physical type sizing, runtime size calculation, and type registration are different stages. registerColumnTypes(), contributeTypes(), and initializeFunctionRegistry() remain lifecycle callbacks, not strategy objects. Resolve version and provider configuration before inherited type registration so those callbacks and all supplied strategies observe one state.

When textual and ordinal named finite-domain types require different DDL, override EnumSupport#getCreateOrdinalTypeCommands(). Its default delegates to getCreateTypeCommands() and is therefore appropriate only when both mappings use the same database representation.

If CurrentTemporalSupport#currentTimestamp() returns a database-specific expression instead of the standard current_timestamp function, also override usesStandardCurrentTimestampFunction() to return false.

Direct Java Time JDBC access

DirectJavaTimeJdbcSupport describes whether the Dialect and its selected driver exchange an exact java.time class directly through JDBC. Scalar support covers both PreparedStatement#setObject() and typed ResultSet#getObject(), together with the corresponding callable-statement operations. Report a class as supported only when both directions work.

The inherited Dialect profile assumes the scalar support required by JDBC 4.2: LocalDate, LocalTime, LocalDateTime, OffsetTime, and OffsetDateTime. Override getDirectJavaTimeJdbcSupport() when a supported driver does not implement part of that contract, or when the driver supports the additional ZonedDateTime or Instant classes. Resolve any driver- or version-sensitive answer during Dialect construction and return one stable, thread-safe profile.

Use DirectJavaTimeJdbcSupports.none(), jdbc42(), all(), or of(…​) instead of depending on an implementation class. The three-set of(…​) overload reports scalar, native JDBC STRUCT, and native JDBC ARRAY support independently:

private static final DirectJavaTimeJdbcSupport DIRECT_JAVA_TIME_JDBC_SUPPORT =
		DirectJavaTimeJdbcSupports.of(
				Set.of(
						LocalDate.class,
						LocalTime.class,
						LocalDateTime.class,
						OffsetTime.class,
						OffsetDateTime.class
				),
				Set.of( LocalDate.class, OffsetTime.class, OffsetDateTime.class ),
				Set.of( LocalDateTime.class )
		);

@Override
public DirectJavaTimeJdbcSupport getDirectJavaTimeJdbcSupport() {
	return DIRECT_JAVA_TIME_JDBC_SUPPORT;
}

None of the scalar-only profiles infer native-container support: jdbc42(), all(), and the varargs of(…​) report no direct STRUCT or ARRAY support. Use the three-set overload only when the driver accepts the exact class while creating and extracting that container kind.

OffsetTime and OffsetDateTime are a matched pair within each access path. Both must be present in the scalar, STRUCT, or ARRAY set to enable either one for that path; omitting either class disables both. Support for OffsetDateTime also lets Hibernate represent a mapped ZonedDateTime as an OffsetDateTime when exact ZonedDateTime access is unavailable. Claim exact ZonedDateTime support only when the driver itself exchanges that class.

Instant is not enabled merely by hibernate.type.java_time_use_direct_jdbc. It is considered when the application selects INSTANT with hibernate.type.preferred_instant_jdbc_type, and still requires the Dialect profile to report Instant support.

This strategy describes Java values at the JDBC boundary. Keep it separate from TimeZoneSupport, which describes database type behavior, and TemporalValueSemantics, which describes precision adjustment and temporal literal offset preservation. If direct access is requested but the profile reports it unsupported, Hibernate falls back to the corresponding physical representation and logs the known limitation once per Java Time class, with the offset pair logged together.

Implementing and contributing Java/JDBC types

A custom basic mapping normally pairs a JavaType with a JdbcType. The Java descriptor owns Java-side equality, wrapping, unwrapping, mutability, and its JDBC recommendation. The JDBC descriptor owns type codes, binding, extraction, and optional SQL-literal formatting. Keep those responsibilities separate even when both descriptors are implemented by the same provider.

The principal provider contracts are:

Concern Contract or base Guidance

Java value semantics

JavaType, BasicJavaType, AbstractJavaType, AbstractClassJavaType

Implement a descriptor or extend the narrowest base whose defaults match the value.

Mutability

MutabilityPlan, MutableMutabilityPlan

Supply the plan from JavaType#getMutabilityPlan(); make immutable plans reusable.

JDBC access

JdbcType, BasicBinder, BasicExtractor

Supply a binder and extractor from the descriptor. Conversion belongs outside those operations.

Parameterized JDBC types

JdbcTypeConstructor

Resolve a descriptor from the element JDBC type and the supplied mapping context.

Aggregate and structured values

AggregateJdbcType, StructuredJdbcType

Treat the registered instance as a prototype, supply the mapping-specific descriptor from resolveAggregateJdbcType(…​), and use AggregateJdbcValues instead of internal struct helpers.

Bootstrap registration

TypeContributor, TypeContributions

Register descriptors during metadata bootstrap and never retain the lifecycle-scoped contributions object.

For independently reusable types, expose a TypeContributor as a Java service or register it programmatically. A contributor may register both sides of the pair and any parameterized constructor in one callback:

public final class ProviderTypeContributor implements TypeContributor {
    @Override
    public void contribute(
            TypeContributions contributions,
            ServiceRegistry serviceRegistry) {
        contributions.contributeJavaType( ProviderJavaType.INSTANCE );
        contributions.contributeJdbcType( ProviderJdbcType.INSTANCE );
        contributions.contributeJdbcTypeConstructor(
                ProviderJdbcTypeConstructor.INSTANCE
        );
    }
}

A Dialect may perform database-specific registration from contributeTypes(…​) using the same callbacks. Invoke inherited registration before adding or replacing descriptors unless the Dialect deliberately owns the complete inherited type set. Resolve version and driver configuration before that inherited call.

For PostgreSQL component-format structured values, extend org.hibernate.dialect.type.spi.AbstractPostgreSQLStructJdbcType. Use its runtime-model constructor for a mapped aggregate; Hibernate resolves the boot-model attribute ordering without exposing the boot mapping as a provider dependency. Implement only the driver binding, extraction, and write-expression variation owned by the provider.

Aggregate JDBC values and native containers

AggregateJdbcType separates a mapped aggregate from the single native value bound to or extracted from the JDBC driver. A provider implementing a custom aggregate or structured type owns the encoding of that native container. For example, a document-oriented driver might encode an Object[] as a native document and decode that document back into component values.

AggregateJdbcValues owns the Hibernate mapping work on either side of that provider codec. Use fromDomainValue(…​) before encoding a native value, use toLogicalJdbcValues(…​) to implement AggregateJdbcType#extractJdbcValues(…​), and use toDomainValue(…​) when a JDBC extractor must return the mapped embeddable value:

public Object createJdbcValue(Object domainValue, WrapperOptions options) throws SQLException {
	return new ExampleDocument(
			AggregateJdbcValues.fromDomainValue( mappingType, domainValue, valueOrder, options )
	);
}

@Override
public Object[] extractJdbcValues(Object rawJdbcValue, WrapperOptions options) throws SQLException {
	return AggregateJdbcValues.toLogicalJdbcValues(
			mappingType,
			( (ExampleDocument) rawJdbcValue ).physicalValues(),
			valueOrder,
			options
	);
}

The no-order overloads use Hibernate’s logical mapping order. If the native container has a different physical order, create an immutable AggregateJdbcValueOrder with the logical value found at each physical position. Thus physicalOrder(2, 0, 1) transforms logical values [A, B, C] to physical values [C, A, B]. The order is validated as a complete permutation and its inverse is calculated once.

Decode the provider-specific container before calling AggregateJdbcValues, and normalize driver-specific scalar anomalies as part of that decoding. The facade handles mapping conversion, nested aggregates, discriminators, and ordering; it does not understand arbitrary native document formats. Do not use the internal StructHelper or expose StructAttributeValues from provider code.

For a provider codec backed by a native JDBC STRUCT or ARRAY, component values produced by AggregateJdbcValues#fromDomainValue(…​) may use direct Java Time classes selected by the mapping. Consult supportsInStruct() or supportsInArray() before passing such a value to the driver. When that container path does not support the exact class, use the public JavaTimeJdbcType physical-type and conversion helpers to obtain the representation expected by the corresponding SQL type. These container capabilities do not apply to textual JSON, XML, or other provider-defined document encodings.

Focused facilities cover common composition needs without exposing their Core implementations:

  • DB2JdbcTypes supplies stable stock descriptors, including the seven Java temporal descriptors with DB2’s null-safe typed-getObject behavior;

  • LobDataExtraction materializes a binary stream or CLOB using Hibernate’s standard closing and exception-conversion behavior. It closes the consumed stream or reader, but the caller remains responsible for freeing the JDBC locator; and

  • EnumRelationalValues returns enum names or converter-produced relational strings in declaration order. Sort the returned fresh array only when the database declaration requires it.

Do not import .internal descriptor implementations or utilities merely to obtain stock behavior. An internal class may change between releases without the X.Y SPI compatibility expectations described in the SPI and Service Provider Guide.

Schema and DDL strategies

Concern Primary contract Dialect supply point Provider guidance

Catalog and schema lifecycle

NamespaceSupport

getNamespaceSupport()

Render create/drop commands without conflating qualification or catalog separators.

Conditional DDL

IfExistsSupport

getIfExistsSupport()

Describe placement independently for each supported database object.

Table alteration and creation

AlterTableSupport, TableCreationSupport

getAlterTableSupport(), getTableCreationSupport()

Return focused fragments and plans which exporters compose.

Column definitions

ColumnDefinitionSupport

getColumnDefinitionSupport()

Own column declaration ordering, nullability, defaults, and generated clauses.

Indexes and constraints

IndexDdlSupport, ConstraintControlSupport

getIndexDdlSupport(), getConstraintControlSupport()

Keep index grammar separate from runtime enable/disable commands.

Truncation and schema drop

TruncateSupport, SchemaDropSupport

getTruncateSupport(), getSchemaDropSupport()

Preserve command ordering and multi-command results.

Foreign keys and checks

ForeignKeySupport, CheckConstraintSupport

getForeignKeySupport(), getCheckConstraintSupport()

Render focused constraint fragments consumed by schema exporters.

Database-object comments

SchemaCommentSupport

getSchemaCommentSupport()

Handle schema-object comments only, not SQL query comments.

Identity and sequences

IdentityColumnSupport, SequenceSupport

getIdentityColumnSupport(), getSequenceSupport()

Keep value generation and DDL/extraction behavior consistent.

Complete object commands

Exporter, TableMigrator, TableCleaner

the get…​Exporter(), getTableMigrator(), and getTableCleaner() methods

Use exporters for complete commands and support strategies for focused pieces.

Temporary-table lifecycle

TemporaryTableStrategy, TemporaryTableExporter

persistent/local/global strategy methods and getTemporaryTableExporter()

Make the selected strategy kind, lifecycle commands, and exporter agree.

NameQualifierSupport, getCatalogSeparator(), and NamespaceSupport are related but independent. The first describes which qualifiers are supported, the second supplies metadata-free catalog punctuation, and the third owns catalog/schema creation and removal.

Query-shape strategies

Query strategies describe independent pieces of database grammar. They remain Dialect capabilities even when the SQL AST translator is their primary consumer.

See the Dialect SQL AST SPI, function SPI, aggregate SPI, and query-hint SPI for the complete contracts.

Concern Primary contract Dialect supply point Provider guidance

Predicates and row values

PredicateSupport, RowValueSupport

getPredicateSupport(), getRowValueSupport()

Describe placement and tuple grammar independently.

Aggregate and window syntax

TupleCountSupport, ExpressionCoercionSupport, WindowFunctionSupport

the corresponding get…​Support() methods

Report exact function forms without creating an aggregate catch-all.

Set operations and subqueries

SetOperationSupport, SubquerySupport

getSetOperationSupport(), getSubquerySupport()

Represent each placement restriction explicitly.

Ordering and grouping

NullOrderingSupport, FunctionalDependencyAnalysisSupport

getNullOrderingSupport(), getFunctionalDependencyAnalysisSupport()

Separate null precedence from group-by functional-dependency rules.

Tableless and synthetic roots

SingleRowTableSupport, SyntheticTableGroupSupport

getSingleRowTableSupport(), getSyntheticTableGroupSupport()

Use a synthetic root only for the query shapes which require one.

Common table expressions

CteSupport

getCteSupport()

Describe placement, recursion, materialization, and mutation capabilities.

DML grammar

MutationSyntaxSupport, DmlTargetColumnQualifierSupport

getMutationSyntaxSupport(), getDmlTargetColumnQualifierSupport()

Keep mutation syntax separate from bulk-mutation execution planning.

Values and fetch clauses

ValuesListSupport, FetchClauseSupport

getValuesListSupport(), getFetchClauseSupport()

Report context support for values independently of list cardinality; an unsupported context is emulated even for one row.

Query hints

QueryHintPlacement

getQueryHintPlacement()

Coordinate placement with the retained focused hint-rendering methods.

Prefer a named standard profile for a standard database rule:

@Override
@SPI({ IMPLEMENT, SUPPLY })
public SyntheticTableGroupSupport getSyntheticTableGroupSupport() {
	return SyntheticTableGroupSupport.SELECT_ONE_FOR_LITERALS;
}

Use builders and immutable value profiles when several independent features must be stated together:

private static final CteSupport CTE_SUPPORT = CteSupport.builder()
		.placement( CteSupport.Placement.TOP_LEVEL )
		.recursiveFeatures( CteSupport.RecursiveFeature.RECURSIVE )
		.mutationFeatures( CteSupport.MutationFeature.NON_QUERY )
		.build();
private static final MultiTableMutationSupport MULTI_TABLE_MUTATION_SUPPORT =
		new MultiTableMutationSupport(
				MultiTableMutationStrategyKind.CTE,
				MultiTableMutationStrategyKind.LOCAL_TEMPORARY_TABLE
		);
private static final MutationSyntaxSupport MUTATION_SYNTAX_SUPPORT = MutationSyntaxSupport.builder()
		.capability( MutationKind.UPDATE, MutationSyntaxCapability.FROM_CLAUSE )
		.capability( MutationKind.DELETE, MutationSyntaxCapability.JOIN )
		.build();

SyntheticTableGroupSupport is the canonical example of this architecture. A focused capability replaced database-specific SQM converters which existed only to add a synthetic table root for literal grouping or ordering.

Function contribution and descriptor contracts

Function registration is a bootstrap lifecycle, not a long-lived strategy. A Dialect contributes its database-specific function set from initializeFunctionRegistry(FunctionContributions). A library or application which contributes functions independently of a Dialect implements FunctionContributor and supplies it through Java service loading, Configuration#registerFunctionContributor(), or MetadataBuilder#applyFunctions().

Both callbacks receive a boot-scoped FunctionContributions. Use it to obtain the SqmFunctionRegistry, TypeConfiguration, and services, complete all registrations before returning, and retain neither the contribution context nor the mutable registry. Independent contributors execute in ascending ordinal order before the Dialect callback. A later registration under the same key replaces an earlier one.

Choose the smallest supported extension level which expresses the function:

Need Contract Provider guidance

A stock Hibernate definition or database variant

CommonFunctionFactory

Construct it inside the callback, invoke the required stock methods, and discard it before returning. The factory supports use, not subclassing or supply.

A simple provider-defined named or pattern function

SqmFunctionRegistry descriptor builders

Configure validation, return-type inference, argument-type inference, and rendering through a builder, then register immediately.

Custom SQM generation

SqmFunctionDescriptor or AbstractSqmFunctionDescriptor

Implement a reusable descriptor and register its stable instance through a descriptor-accepting supply point.

Custom self-rendering SQL

FunctionRenderer or AbstractSqmSelfRenderingFunctionDescriptor

Own SQL AST rendering in the descriptor. Use a supported focused Dialect function base when its existing behavior is the intended starting point.

For example, a Dialect may combine a stock definition, a simple pattern, and a provider-owned descriptor in the same callback:

@Override
public void initializeFunctionRegistry(FunctionContributions contributions) {
    super.initializeFunctionRegistry( contributions );

    new CommonFunctionFactory( contributions ).cot();
    contributions.getFunctionRegistry()
            .patternDescriptorBuilder( "provider_concat", "(?1 || ?2)" )
            .setExactArgumentCount( 2 )
            .register();
    contributions.getFunctionRegistry().register(
            "provider_self_rendering",
            new ProviderSelfRenderingFunctionDescriptor()
    );
}

The selected general, array, and JSON descriptor bases are supported where their Javadocs declare IMPLEMENT; the package as a whole is not an implementation contract. CommonFunctionFactory is a supported catalog of stock registrations for the current Hibernate X.Y release family. Adding a stock method is compatible, while removing or incompatibly changing an existing stock method is not. Its instances are callback-local helpers and are never supplied to Hibernate.

See the FunctionContributor, SqmFunctionDescriptor, SqmFunctionRegistry, and CommonFunctionFactory contracts for exact roles, constructors, and supply points.

Execution, locking, and mutation strategies

Concern Primary contract Dialect supply point Provider guidance

Pagination

LimitHandler

getLimitHandler()

Use a standard handler or implement the request/result contract for custom SQL and JDBC instructions.

Query locking

LockingSupport, LockingClauseStrategy

getLockingSupport(), getLockingClauseStrategy(…​)

Use the stable profile for capabilities and the per-request strategy for clause selection.

Transaction concurrency

TransactionConcurrency, TransactionConcurrencyResolver

LockingSupport#getTransactionConcurrencyResolver()

Resolve the factory’s read guarantees and operation conflicts from database configuration and available bootstrap facts.

Entity locking

EntityLockingStrategyFactory

getEntityLockingStrategyFactory()

Create a strategy for the supplied request without retaining bootstrap state.

Bulk mutation fallback

MultiTableMutationSupport

getMultiTableMutationSupport()

Choose CTE or temporary/persistent-table handling separately for operation families.

Generated values and row locators

GeneratedValuesSupport, RowIdSupport

getGeneratedValuesSupport(), getRowIdSupport()

Coordinate SQL expressions, retrieval, JDBC typing, and physical declaration; return RowIdSupports.none() when a variant lacks an inherited row locator.

Row-level security

RowLevelSecurity

getRowLevelSecurity()

Keep tenant predicates and supporting DDL in one coherent strategy.

Callable statements and cursors

CallableStatementSupport, RefCursorSupportFactory

getCallableStatementSupport(), getRefCursorSupportFactory()

Separate callable syntax from cursor registration/extraction lifecycle.

Multi-key loading

MultiKeyLoadSizingStrategy

getMultiKeyLoadSizingStrategy(), getBatchLoadSizingStrategy()

Respect parameter limits when choosing batch sizes.

A custom focused implementation may still be a stable singleton:

@Override
public LockingSupport getLockingSupport() {
	return ExampleLockingSupport.INSTANCE;
}

Choosing a locking profile

Dialect#getLockingSupport() supplies one cohesive, stable profile for the Dialect lifetime. Its metadata, clause renderer, table-hint renderer, completed-SQL rewriter, follow-on policy, connection-timeout strategy, and transaction-concurrency resolver must describe the same database behavior. The profile and all reusable components must be immutable or thread-safe; per-query target collection belongs to the LockingClauseStrategy created for that translation.

Choose the narrowest supported approach:

  • Use a named StandardLockingSupports factory when extending a maintained database family. These factories preserve Hibernate’s version thresholds and coordinated syntax, timeout behavior, and concurrency resolution.

  • Use StandardLockingSupports.simple(…​) when every timeout has one classification, or parameterized(…​) when wait, no-wait, and skip-locked differ. These factories return the SPI interface and do not expose the Hibernate-owned implementation.

  • Implement LockingSupport only when the database requires behavior which cannot be expressed by those profiles, such as custom table hints, raw-SQL placement, or follow-on policy. Keep the implementation provider-owned and return stable components.

A follow-on policy reports whether follow-on locking is required; it does not guarantee that a particular execution plan supplies the actions needed to perform it. For completed or raw SQL without those actions, ALLOW proceeds without Hibernate-applied pessimistic locking, FORCE fails, and DISALLOW fails when either the policy or SQL-rewrite outcome requires follow-on locking. IGNORE likewise proceeds without Hibernate-applied locking when follow-on is required.

For statement-clause translation, use StandardLockingClauseStrategies.none() when locking is expressed elsewhere. Use standard(…​) to combine a provider-owned LockingClauseRenderer with Hibernate’s standard root and join target collection. A custom strategy is appropriate only when target collection itself differs.

A provider-owned connection-timeout strategy should use ConnectionLockTimeoutOperations for query and command execution so it participates in Hibernate’s SQL logging, statement observation, resource closing, and exception conversion. MySQL-compatible providers may use StandardConnectionLockTimeoutStrategies.mysql(waitForeverValue) when their only variation is the value representing an effectively unbounded wait.

Resolving transaction concurrency

TransactionConcurrency describes database behavior for the factory’s connections. Hibernate uses its answers when implementing lock modes, including optimistic version checks and stateless-session locking. JDBC isolation constants alone do not establish these answers: for example, SQL Server’s read-committed behavior also depends on READ_COMMITTED_SNAPSHOT.

Supply a provider-owned TransactionConcurrencyResolver through LockingSupport#getTransactionConcurrencyResolver(), or use a StandardLockingSupports.simple(…​) or parameterized(…​) overload accepting a resolver. Named database-family factories already supply their associated resolvers. The default resolver is conservative; it does not infer database guarantees from the availability of locking syntax. The deprecated LockingSupport.Metadata#readsWaitForUncommittedWrites() flag does not supply the concurrency descriptor.

The resolver is reusable, but its result belongs to the factory’s JdbcEnvironment and is available from JdbcEnvironment#getTransactionConcurrency(). Do not store the resolved result on a potentially shared Dialect or locking profile. Return an immutable descriptor; TransactionConcurrencies.builder(…​) is available for constructing one. Connection isolation and relevant database settings must remain consistent across the factory. This contract describes that configuration; it does not change connections or coordinate active sessions.

Bootstrap and declarations

Resolution runs with the connection already borrowed by bootstrap, or with a null connection when JDBC access is unavailable. Use the supplied metadata, established database facts, and any declaration to resolve behavior. Leave missing facts unknown. In particular, a database’s default isolation does not prove which isolation the factory’s connections use.

When a connection is supplied and JdbcMetadataOnBoot permits access, query it for settings needed to distinguish configurations. Do not acquire another connection, retain or close the supplied connection, or change its settings. Close statements and result sets created by the resolver. A failed supported probe may leave facts unknown under ALLOW; under REQUIRE, it must fail resolution. A fact for which no probe is supported may remain unknown under either policy.

hibernate.transaction.concurrency, defined by TransactionSettings.TRANSACTION_CONCURRENCY, accepts a named declaration or a TransactionConcurrency instance. The setting’s Javadoc lists the standard names and their meanings. Providers may document additional names. A name supplies otherwise unavailable facts, including when JDBC access is disabled; it must not override contradictory observations or established database facts. Reject invalid names, contradictions, and required probe failures with TransactionConcurrencyResolutionException so bootstrap fallback does not hide them.

A supplied descriptor instance is authoritative: bootstrap uses it unchanged without invoking the resolver. Direct resolver calls receiving an instance must also return it unchanged without probing or validation. The caller is responsible for its accuracy and immutability. Neither form of declaration configures connection isolation.

Operations, guarantees, and rendered SQL

Describe actual database operations using Operation, independently of the Hibernate lock mode which requested them:

  • READ is an ordinary read under the configured isolation. It may itself acquire locks or provide current committed state.

  • CURRENT_READ is the explicit strategy for reading current committed state, resolving conflicting uncommitted writes before succeeding. It may instead fail on a conflict, and need not retain a lock after the statement.

  • SHARED_LOCK_READ and UPDATE_LOCK_READ describe the actual row lock acquired. Implementing PESSIMISTIC_READ with an update lock does not establish support for SHARED_LOCK_READ.

  • WRITE modifies or deletes an existing row and has no read guarantees.

getBlockingDuration(precedingOperation, concurrentOperation) describes how long protection established by the first operation can exclude the second operation in another transaction on the same existing row. Direction matters. This is a static database rule, not a measurement of active sessions or elapsed wait time. UNKNOWN and CONDITIONAL do not prove exclusion. Predicate locks, phantoms, schema locks, escalation, and incidental internal waits are outside this contract.

Use ReadGuarantees to distinguish stable snapshot visibility, current committed state, prevention of concurrent modification, and a row lock retained until transaction completion. A stable snapshot or possible serialization failure does not establish protection against modification. Report only guarantees which Hibernate may rely on for the resolved configuration.

Keep the explicit CURRENT_READ guarantees consistent with LockingSupport#renderCurrentReadTableHint(String, TransactionConcurrency) and renderCurrentReadClause(TransactionConcurrency). Hibernate first checks whether ordinary READ already provides isCurrentRead(). Otherwise, it requires a supported CURRENT_READ with that guarantee before requesting the explicit strategy. The default renderers use the profile’s share-lock hint or clause; override them when the database needs a different strategy. A cheaper hint which waits for writes but retains no lock must not advertise transaction-long protection.

Provider tests should cover resolution with and without JDBC, named declarations and contradictions, authoritative descriptor instances, and optional versus required probe failures. Test configuration variants affecting concurrency, and verify the claimed read guarantees and directional conflicts using separate transactions against the database, including the SQL rendered for explicit reads.

Cross-strategy consistency

Focused strategies remain independent, but some combinations must describe one valid database configuration.

Supplied behavior Must agree with

Transaction concurrency descriptor

The configured isolation and database settings, actual locks acquired, and current-read hints or clauses rendered by the locking profile.

Multi-table mutation profile

The selected CTE and local/global/persistent temporary-table capabilities.

Temporary-table strategy

The temporary-table exporter and its create, drop, truncate, and lifecycle commands.

Temporal-table support

Type sizing and check-constraint support used to build its DDL profile.

Identifier helper

Identifier, keyword, and JDBC metadata behavior.

Native parameter markers

Resolved driver/version behavior and parameter limits.

Schema exporters

The focused schema, constraint, comment, and IF EXISTS strategies they compose.

SQL AST translator

Every focused capability consulted by the standard translation pipeline.

For example, the fixture’s asymmetric bulk-mutation profile selects a local temporary table for one operation family, so the Dialect supplies the matching local strategy:

@Override
public TemporaryTableStrategy getLocalTemporaryTableStrategy() {
	return ExampleLocalTemporaryTableStrategy.INSTANCE;
}

When a complete built-in vendor profile matches the provider’s database, use TemporaryTableStrategies.db2Global(), hsqlLocal(), mysqlLocal(), oracleLocal(), or sqlServerLocal(). These accessors return the public TemporaryTableStrategy contract; do not depend on the concrete class or its identity. Implement a provider-owned strategy when any naming, DDL, column, or lifecycle answer differs.

A CTE selection likewise requires the appropriate non-query CTE feature. Configured global or entity-specific custom mutation strategies retain their documented precedence over the Dialect fallback profile.

SQL AST translation and the global HQL/SQM boundary

Dialect#getSqlAstTranslatorFactory() is the only general translator factory supplied by a Dialect. Returning null selects StandardSqlAstTranslatorFactory. A custom factory should normally extend that standard factory, remain reusable and thread-safe, retain no translation request, and create a fresh single-use translator for every request.

@Override
@SPI({ IMPLEMENT, SUPPLY })
public SqlAstTranslatorFactory getSqlAstTranslatorFactory() {
	return ExampleSqlAstTranslatorFactory.INSTANCE;
}

Override a focused strategy first when the variation is declarative. Supply a translator subclass only when rendering requires structural behavior or control flow which the focused strategies cannot express. A custom translator must still honor every applicable strategy supplied by its Dialect.

For a JDBC driver whose command language is fundamentally different from SQL, the factory may instead return a direct SqlAstTranslator implementation. Use AbstractSqlAstWalker as the structural traversal base and own the command representation explicitly. This is appropriate, for example, for a document database exposed through a JDBC driver; the generated command text need only be understood by that driver.

public final class ExampleDirectSqlAstTranslator<T extends JdbcOperation>
		extends AbstractSqlAstWalker
		implements SqlAstTranslator<T> {
	private final SqlAstTranslationRequest<? extends Statement, T> request;
	private final List<JdbcParameterBinder> parameterBinders = new ArrayList<>();
	private final Set<String> affectedQuerySpaces = new LinkedHashSet<>();
	private final Stack<Clause> clauseStack = new StandardStack<>();
	private JdbcParameterBindings jdbcParameterBindings;
	private QueryPart currentQueryPart;

	public ExampleDirectSqlAstTranslator(SqlAstTranslationRequest<? extends Statement, T> request) {
		this.request = request;
	}

	@Override
	@SuppressWarnings("unchecked")
	public T translate(JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) {
		this.jdbcParameterBindings = jdbcParameterBindings;
		if ( request instanceof SqlAstTranslationRequest.Select selectRequest ) {
			selectRequest.statement().accept( this );
			return (T) JdbcOperations.select( selectRequest )
					.command( mongoCommand( "find" ) )
					.parameterBinders( parameterBinders )
					.affectedQuerySpaces( affectedQuerySpaces )
					.build();
		}
		if ( request instanceof SqlAstTranslationRequest.QueryMutation mutationRequest ) {
			mutationRequest.statement().accept( this );
			return (T) JdbcOperations.queryMutation( mutationRequest )
					.command( mongoCommand( mutationVerb( mutationRequest ) ) )
					.parameterBinders( parameterBinders )
					.affectedQuerySpaces( affectedQuerySpaces )
					.build();
		}
		if ( request instanceof SqlAstTranslationRequest.ModelMutation<?> modelRequest ) {
			final TableMutation<?> mutation = modelRequest.statement();
			addAffectedTableName( mutation.getTableName() );
			mutation.forEachParameter( parameter -> collectParameter( parameter ) );
			return (T) mutation.createMutationOperation(
					mongoCommand( mutation.getClass().getSimpleName() ),
					parameterBinders
			);
		}
		throw new IllegalArgumentException( "Unsupported translation request: " + request );
	}

	private void collectParameter(JdbcParameter parameter) {
		parameterBinders.add( parameter.getParameterBinder() );
	}

	private String mutationVerb(SqlAstTranslationRequest.QueryMutation mutationRequest) {
		if ( mutationRequest.statement() instanceof InsertSelectStatement ) {
			return "insert";
		}
		if ( mutationRequest.statement() instanceof UpdateStatement ) {
			return "update";
		}
		if ( mutationRequest.statement() instanceof DeleteStatement ) {
			return "delete";
		}
		throw new IllegalArgumentException(
				"Unsupported query mutation: " + mutationRequest.statement().getClass().getName()
		);
	}

	private String mongoCommand(String operation) {
		final String collection = affectedQuerySpaces.isEmpty() ? "unknown" : affectedQuerySpaces.iterator().next();
		return "{ \"" + operation + "\": \"" + collection + "\" }";
	}

	@Override
	public Statement getSqlAst() {
		return request.statement();
	}

	@Override
	public SessionFactoryImplementor getSessionFactory() {
		return request.sessionFactory();
	}

	@Override
	@SuppressWarnings("unchecked")
	public <X> X getLiteralValue(Expression expression) {
		if ( expression instanceof Literal literal ) {
			return (X) literal.getLiteralValue();
		}
		if ( expression instanceof JdbcParameter parameter && jdbcParameterBindings != null ) {
			return (X) jdbcParameterBindings.getBinding( parameter ).getBindValue();
		}
		throw new IllegalArgumentException( "Expression is not a bound literal: " + expression );
	}

	@Override
	public void renderNamedSetReturningFunction(
			String functionName,
			List<? extends SqlAstNode> sqlAstArguments,
			SetReturningFunctionType tupleType,
			String tableIdentifierVariable,
			SqlAstNodeRenderingMode argumentRenderingMode) {
		throw new UnsupportedOperationException( "Named set-returning functions are not supported" );
	}

	@Override
	public void render(SqlAstNode sqlAstNode, SqlAstNodeRenderingMode renderingMode) {
		sqlAstNode.accept( this );
	}

	@Override
	public QueryPart getCurrentQueryPart() {
		return currentQueryPart;
	}

	@Override
	public Stack<Clause> getCurrentClauseStack() {
		return clauseStack;
	}

	@Override
	public Set<String> getAffectedTableNames() {
		return Set.copyOf( affectedQuerySpaces );
	}

	@Override
	public void addAffectedTableName(String tableName) {
		affectedQuerySpaces.add( tableName );
	}

	@Override
	public void visitQuerySpec(QuerySpec querySpec) {
		final QueryPart previous = currentQueryPart;
		currentQueryPart = querySpec;
		try {
			super.visitQuerySpec( querySpec );
		}
		finally {
			currentQueryPart = previous;
		}
	}

	@Override
	public void visitQueryGroup(QueryGroup queryGroup) {
		final QueryPart previous = currentQueryPart;
		currentQueryPart = queryGroup;
		try {
			super.visitQueryGroup( queryGroup );
		}
		finally {
			currentQueryPart = previous;
		}
	}

	@Override
	public void visitNamedTableReference(NamedTableReference tableReference) {
		addAffectedTableName( tableReference.getTableExpression() );
	}

	@Override
	public void visitParameter(JdbcParameter jdbcParameter) {
		parameterBinders.add( jdbcParameter.getParameterBinder() );
	}

	@Override
	public void visitDeleteStatement(DeleteStatement statement) {
		statement.getTargetTable().accept( this );
		super.visitDeleteStatement( statement );
	}

	@Override
	public void visitUpdateStatement(UpdateStatement statement) {
		statement.getTargetTable().accept( this );
		super.visitUpdateStatement( statement );
	}

	@Override
	public void visitInsertStatement(InsertSelectStatement statement) {
		statement.getTargetTable().accept( this );
		super.visitInsertStatement( statement );
	}
}

Direct translators should create their executable query descriptions with JdbcOperations. Its typed select() and queryMutation() builders return only SPI interfaces and hide Hibernate’s operation implementations. Supply parameter binders in command-placeholder order, and identify affected relational tables or their backend-equivalent query spaces so cache invalidation remains correct.

When a translated command renders query-options-backed pagination placeholders, create them with JdbcParameterFactory. Its query-limit and query-offset factories produce parameters whose binders read the original QueryOptions instead of ordinary JdbcParameterBindings. Add each binder in command-placeholder order and pass the same parameter instance to the corresponding JdbcOperations.SelectBuilder pagination method. The overloads accepting TypeConfiguration resolve Hibernate’s persistence-unit-scoped Integer basic type; use the BasicType<Integer> overload only when that type has already been resolved from the current persistence unit. For other execution-context-backed values, custom() creates a parameter expression from a JDBC mapping and provider-owned binder without exposing Hibernate internals.

SqlAstTranslationRequest.ModelMutation is deliberately not accepted by either JdbcOperations builder. It represents mapping-model work originating from flush, not a query-language mutation. After translating its TableMutation, call TableMutation#createMutationOperation(command, parameterBinders) so the mapping model retains ownership of the concrete operation, expectation, callable state, and table metadata.

Translator subclasses may call getVersionSeedMapping(Expression) to recognize Hibernate’s generated version seed and obtain its public mapping, or isParameterInterpretation(Expression) when behavior must distinguish an SQM parameter interpretation from an ordinary JDBC parameter. Both methods are final provider operations, not override points. To append an expression to a SelectClause without constructing an internal selection implementation, call addSqlSelection(Expression).

Provider expressions and full-MERGE translators

For a provider-owned SQL AST expression, extend AbstractSelfRenderingExpression instead of directly implementing the low-level SelfRenderingExpression protocol. Supply the nullable mapping type once; the base retains it, owns final visitor dispatch, and leaves one rendering callback to the provider:

public final class ExampleSelfRenderingExpression extends AbstractSelfRenderingExpression {
	/// Creates a fixture expression with the supplied mapping type.
	///
	/// @since 8.0
	public ExampleSelfRenderingExpression(@Nullable JdbcMappingContainer expressionType) {
		super( expressionType );
	}

	/// Renders the deterministic fixture expression.
	///
	/// @since 8.0
	@Override
	public void renderToSql(
			SqlAppender sqlAppender,
			SqlAstTranslator<?> translator,
			SessionFactoryImplementor sessionFactory) {
		sqlAppender.appendSql( "fixture_expression" );
	}
}

For databases whose optional-table mutations use a full insert/update/delete MERGE, extend SqlAstTranslatorWithMerge. Override the smallest protected renderMerge…​ callback which expresses the grammar difference:

public final class ExampleMergeSqlAstTranslator<T extends JdbcOperation> extends SqlAstTranslatorWithMerge<T> {
	/// Creates a fixture translator for one translation request.
	///
	/// @since 8.0
	public ExampleMergeSqlAstTranslator(SqlAstTranslationRequest<? extends Statement, T> request) {
		super( request );
	}

	/// Renders the fixture's target alias without an `as` keyword.
	///
	/// @since 8.0
	@Override
	protected void renderMergeTargetAlias() {
		appendSql( "fixture_target" );
	}
}

createMergeOperation() is final because Hibernate owns SQL collection, parameter ordering, expectation selection, and operation construction. The full-MERGE base does not imply that the adjacent SqlAstTranslatorWithUpsert is a supported provider base.

Dialect#getHqlTranslator() and Dialect#getSqmTranslatorFactory() no longer exist. HQL parsing and general SQM conversion are query-engine-wide concerns, not Dialect supply points. Framework integrations which intentionally replace them use the global settings hibernate.query.hql.translator and hibernate.query.sqm.translator, exposed downstream by QueryEngineOptions#getCustomHqlTranslator() and getCustomSqmTranslatorFactory().

There is no service-loaded or Dialect-level fallback for those global translators. The standalone fixture’s ExampleSqmTranslatorFactory verifies the global request-based integration contract; ExampleDialect does not supply it.

A database-free contract profile

org.hibernate.orm:hibernate-dialect-testkit boots a fixed mapping model with JDBC metadata access disabled. The profile supplies a fresh Dialect, its expected version, optional safe settings, and applicability decisions for optional contracts.

public final class ExampleDialectContractProfile implements DialectContractProfile {
	@Override
	public String name() {
		return "Example Dialect";
	}

	@Override
	public Dialect createDialect() {
		return new ExampleDialect();
	}

	@Override
	public DatabaseVersion expectedDatabaseVersion() {
		return DatabaseVersion.make( 1 );
	}
}

Profile names must be stable and nonblank. createDialect() must return a new instance on every call. Settings must not select a different Dialect, request JDBC metadata, provide a connection, or execute schema commands.

Running the generic suite

With the Gradle plugin, name each provider-owned profile in hibernateDialectProvider.contractProfiles. The generated bridge constructs profiles in declared order and calls DialectTestKit.contractTests(profile); it does not generate the profile itself and is never packaged in the provider jar. Run the complete provider verification with:

./gradlew verifyDialectProvider

The constituent tasks are validateDialectProviderBoundaries and dialectProviderTest. Boundary reports are written to build/reports/hibernate-dialect-provider/boundary-validation.txt and boundary-validation.json. Set attachToCheck = false only when another lifecycle task deliberately invokes verifyDialectProvider.

An INTERNAL_TARGET finding is a warning: it identifies a dependency with no release-to-release compatibility guarantee, but does not fail validation by default. A MISSING_IMPLEMENT_ROLE finding is an error because the provider is extending, implementing, or overriding a declaration which Hibernate does not support as a provider extension point. Set warningsAsErrors = true when a provider wants internal-use warnings to fail its build. Strict mode changes only the build outcome; reports continue to identify those findings as WARNING.

A provider-owned SPI contract may extend or implement a Hibernate API contract, or redeclare one of its methods. The validator recognizes provider SPI by an spi package component or a direct @SPI annotation. This source-sensitive allowance lets a provider compose its own supported contract from Hibernate’s stable API; it does not make the Hibernate API an implementation extension point for ordinary provider classes. Extending, implementing, or overriding a Hibernate SPI still requires the target contract to declare IMPLEMENT.

Reviewing internal-use warnings

An internal-use warning is never a compatibility promise. It means the provider has chosen to depend on an implementation detail which may change in any release. The preferred response is to migrate to a documented API or SPI and make the warning disappear.

Hibernate’s community Dialects are checked with the same provider-boundary engine. Their remaining internal-use warnings stay visible in the generated report and carry the same lack of compatibility guarantees. The published Gradle plugin intentionally has no allowlist or suppression mechanism.

Common supported migrations include:

  • use org.hibernate.jdbc.spi.JdbcExceptionHelper to inspect a JDBC exception chain instead of importing the former internal utility;

  • select complete vendor profiles through TemporaryTableStrategies instead of importing vendor implementations from org.hibernate.dialect.temptable.internal; and

  • use the focused translator operations described below instead of naming internal SQL AST expression or selection implementations.

For Maven, Ant, or intentionally manual test-kit use, expose one provider-owned JUnit test factory:

@TestFactory
DynamicContainer dialectContracts() {
	return DialectTestKit.contractTests( new ExampleDialectContractProfile() );
}

The required contracts cover connectionless bootstrap, basic select and DML translation, identifier and literal rendering, JDBC parameter order, tableless and synthetic roots, and schema DDL. Optional contracts cover pagination, locking, temporary tables, and multi-table mutation. A required contract cannot be skipped. Mark an optional contract inapplicable only with a nonblank reason.

These tests verify internal consistency and provider integration. They do not certify SQL by executing it against a database. Keep live-database and container tests in the provider’s own test matrix.

Provider-specific assertions

Use DialectTestKit.openContext(profile) for behavior unique to the provider:

@Test
void providerSpecificLiteralRendering() {
	try ( DialectTestContext context = DialectTestKit.openContext(
			new ExampleDialectContractProfile() ) ) {
		assertTrue( context.translate(
				"select e.id from ContractEntity e where e.name = 'fixture'"
		).statements().get( 0 ).sql().contains( "fixture('fixture')" ) );
	}
}

The context is thread-confined and must be closed. translate() accepts named parameters, optional pagination, and a whole-query lock mode. Results are immutable and include every generated statement and JDBC parameter position. Positional HQL parameters and alias-specific lock modes are intentionally outside this initial contract.

Packaging and compatibility workflow

Before publishing a provider release:

  1. compile against the oldest and newest supported Hibernate versions;

  2. run the generic contract profile on each version;

  3. run provider-specific SQL assertions;

  4. run the provider’s live database matrix;

  5. inspect deprecation, incubation, SPI, and migration reports;

  6. document the exact Hibernate and database versions supported.

Do not shade Hibernate Core, expose .internal types in provider signatures, or copy Hibernate implementation classes into the provider artifact.

The Gradle validator reads https://docs.hibernate.org/orm/X.Y/metadata/classifications.json.gz, where X.Y is the resolved Core family. It authenticates the gzip bytes with the adjacent SHA-256 file. Use classificationMetadataFile for an unpublished family or an online-independent build, and populate the Gradle cache online before using --offline. A plugin/Core family mismatch is an error; exact maintenance versions within one family need not be identical.

Troubleshooting

Connection requested during profile boot

Remove metadata-dependent settings and resolve provider configuration before inherited type registration.

Unknown HQL entity

Use the fixed ContractEntity model in test-kit requests; provider mappings are not installed in the generic context.

Parameter order mismatch

Inspect GeneratedParameter.jdbcPosition() rather than counting ? characters; a Dialect may expand or add internal binders.

Unexpected SQL family

Identify the supplying Dialect method and test the focused strategy before replacing a translator.

INTERNAL_TARGET warning

Replace the upstream .internal dependency with a documented API/SPI contract; moving provider code into a package named internal does not change ownership. The warning means the dependency has no compatibility guarantee across releases.

MISSING_IMPLEMENT_ROLE error

Extend, implement, or override a Hibernate SPI only when it is classified with IMPLEMENT. A provider SPI may compose a Hibernate API, but an ordinary provider implementation may not treat API visibility as subclassing or implementation permission.

Offline metadata miss

Run once online or configure classificationMetadataFile with a locally generated or mirrored family file.

Live database accepts different SQL

Keep the database result as the final authority and add a focused provider assertion plus a live integration test.