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
|
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.DialectResolverorg.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.DialectSelectororg.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 |
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
@seelinks connect the two declarations; -
IMPLEMENTdoes not implySUPPLY, andSUPPLYdoes 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:
-
Return a named standard constant or immutable profile.
-
Construct a profile with its supported builder or factory.
-
Compose or implement the focused strategy.
-
Override a narrowly classified rendering hook when its contract explicitly supports provider implementation.
-
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 |
|
|
Describe quoting and identifier-length rules independently from JDBC metadata. |
Identifier normalization |
|
|
Refine the supplied builder and use |
Keyword behavior |
|
|
Use the strategy for keyword policy and the callback for registration lifecycle. |
Literal rendering |
|
|
Implement only database-specific literal forms and delegate the remainder. |
Metadata correction |
|
|
Override unreliable driver answers without pretending that metadata was read. |
Parameter limits and markers |
|
|
Keep driver-sensitive marker selection consistent with the configured driver. |
String-value semantics |
|
|
Describe empty-string, trailing-space, and comparison behavior as one profile. |
Result-column aliases |
|
|
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
The central SQL AST and translator contracts, translator factories and requests,
|
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 |
|
|
Report supported capacities and precision defaults; use |
Runtime size resolution |
|
|
Customize mapped-size calculation without changing the physical capability profile. |
Direct Java Time JDBC access |
|
|
Report exact Java classes accepted for scalar access and independently inside native JDBC |
Enum declarations and checks |
|
|
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 |
|
|
Describe native array behavior; separate it from aggregate-column support. |
LOB behavior |
|
|
Coordinate LOB type classification, materialization, binding, and merge behavior. |
Untyped null binding |
|
|
Choose the complete binding policy instead of independent Boolean answers. |
Nationalized and zoned types |
|
|
Report type-system behavior, not temporal literal syntax. |
Current date and time |
|
|
Supply current temporal expressions and database-side timestamp retrieval; report whether the timestamp expression uses standard |
Temporal formatting |
|
|
Translate format patterns without absorbing arithmetic or value semantics. |
Temporal arithmetic |
|
|
Own timestamp add/diff patterns and other operation grammar. |
Temporal values |
|
|
Describe precision loss, rounding, and offset preservation. |
System-versioned tables |
|
|
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 |
|
Implement a descriptor or extend the narrowest base whose defaults match the value. |
Mutability |
|
Supply the plan from |
JDBC access |
|
Supply a binder and extractor from the descriptor. Conversion belongs outside those operations. |
Parameterized JDBC types |
|
Resolve a descriptor from the element JDBC type and the supplied mapping context. |
Aggregate and structured values |
|
Treat the registered instance as a prototype, supply the mapping-specific descriptor from |
Bootstrap registration |
|
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 |
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:
-
DB2JdbcTypessupplies stable stock descriptors, including the seven Java temporal descriptors with DB2’s null-safe typed-getObjectbehavior; -
LobDataExtractionmaterializes 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 -
EnumRelationalValuesreturns 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
See the schema SPI, constraint SPI, namespace SPI, identity SPI, sequence SPI, and temporary-table SPI for the complete contracts.
| Concern | Primary contract | Dialect supply point | Provider guidance |
|---|---|---|---|
Catalog and schema lifecycle |
|
|
Render create/drop commands without conflating qualification or catalog separators. |
Conditional DDL |
|
|
Describe placement independently for each supported database object. |
Table alteration and creation |
|
|
Return focused fragments and plans which exporters compose. |
Column definitions |
|
|
Own column declaration ordering, nullability, defaults, and generated clauses. |
Indexes and constraints |
|
|
Keep index grammar separate from runtime enable/disable commands. |
Truncation and schema drop |
|
|
Preserve command ordering and multi-command results. |
Foreign keys and checks |
|
|
Render focused constraint fragments consumed by schema exporters. |
Database-object comments |
|
|
Handle schema-object comments only, not SQL query comments. |
Identity and sequences |
|
|
Keep value generation and DDL/extraction behavior consistent. |
Complete object commands |
|
the |
Use exporters for complete commands and support strategies for focused pieces. |
Temporary-table lifecycle |
|
persistent/local/global strategy methods and |
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 |
|
|
Describe placement and tuple grammar independently. |
Aggregate and window syntax |
|
the corresponding |
Report exact function forms without creating an aggregate catch-all. |
Set operations and subqueries |
|
|
Represent each placement restriction explicitly. |
Ordering and grouping |
|
|
Separate null precedence from group-by functional-dependency rules. |
Tableless and synthetic roots |
|
|
Use a synthetic root only for the query shapes which require one. |
Common table expressions |
|
|
Describe placement, recursion, materialization, and mutation capabilities. |
DML grammar |
|
|
Keep mutation syntax separate from bulk-mutation execution planning. |
Values and fetch clauses |
|
|
Report context support for |
Query hints |
|
|
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 |
|
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 |
|
Configure validation, return-type inference, argument-type inference, and rendering through a builder, then register immediately. |
Custom SQM generation |
|
Implement a reusable descriptor and register its stable instance through a descriptor-accepting supply point. |
Custom self-rendering SQL |
|
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
See the locking SPI, pagination SPI, bulk-mutation SPI, generated-values SPI, row-id SPI, and row-security SPI for the complete contracts.
| Concern | Primary contract | Dialect supply point | Provider guidance |
|---|---|---|---|
Pagination |
|
|
Use a standard handler or implement the request/result contract for custom SQL and JDBC instructions. |
Query locking |
|
|
Use the stable profile for capabilities and the per-request strategy for clause selection. |
Transaction concurrency |
|
|
Resolve the factory’s read guarantees and operation conflicts from database configuration and available bootstrap facts. |
Entity locking |
|
|
Create a strategy for the supplied request without retaining bootstrap state. |
Bulk mutation fallback |
|
|
Choose CTE or temporary/persistent-table handling separately for operation families. |
Generated values and row locators |
|
|
Coordinate SQL expressions, retrieval, JDBC typing, and physical declaration; return |
Row-level security |
|
|
Keep tenant predicates and supporting DDL in one coherent strategy. |
Callable statements and cursors |
|
|
Separate callable syntax from cursor registration/extraction lifecycle. |
Multi-key loading |
|
|
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
StandardLockingSupportsfactory 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, orparameterized(…)when wait, no-wait, and skip-locked differ. These factories return the SPI interface and do not expose the Hibernate-owned implementation. -
Implement
LockingSupportonly 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:
-
READis an ordinary read under the configured isolation. It may itself acquire locks or provide current committed state. -
CURRENT_READis 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_READandUPDATE_LOCK_READdescribe the actual row lock acquired. ImplementingPESSIMISTIC_READwith an update lock does not establish support forSHARED_LOCK_READ. -
WRITEmodifies 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 |
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.
|
|
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.JdbcExceptionHelperto inspect a JDBC exception chain instead of importing the former internal utility; -
select complete vendor profiles through
TemporaryTableStrategiesinstead of importing vendor implementations fromorg.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:
-
compile against the oldest and newest supported Hibernate versions;
-
run the generic contract profile on each version;
-
run provider-specific SQL assertions;
-
run the provider’s live database matrix;
-
inspect deprecation, incubation, SPI, and migration reports;
-
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
ContractEntitymodel 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_TARGETwarning-
Replace the upstream
.internaldependency with a documented API/SPI contract; moving provider code into a package namedinternaldoes not change ownership. The warning means the dependency has no compatibility guarantee across releases. MISSING_IMPLEMENT_ROLEerror-
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
classificationMetadataFilewith 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.