Vendor notes¶
The library is vendor-aware for the databases it is tested on and conservative for the rest. A vendor profile
holds the database-specific facts the engine needs: how to stream, which limits to respect, how NULLs sort. The profile
is resolved once per EntityManagerFactory and logged once at INFO with how it was resolved, because a wrong profile
gives correct-looking results with the wrong limits.
Support tiers¶
| Tier | Database | Versions | Tested |
|---|---|---|---|
| 1 | H2 | 2.2+, native mode | every pull request |
| 1 | PostgreSQL | 14, 15, 16, 17 | every pull request (Testcontainers) |
| 1 | MySQL | 8.0, 8.4 | every pull request (Testcontainers) |
| 3 (community) | Oracle, SQL Server, others | profile contributions welcome |
A Tier-1 release needs the whole compatibility suite to pass on every Tier-1 version. A Tier-3 profile is community-supported and never gates a release.
What a profile decides¶
| Concern | H2 | PostgreSQL | MySQL |
|---|---|---|---|
| Streaming | positive fetch size | positive fetch size; the driver uses a cursor only when autocommit is off, so run in a transaction | row-by-row by default, or cursor fetch by configuration |
| Query timeout | jakarta.persistence.query.timeout |
the same | the same |
IN list size (soft limit) |
10 000 | 10 000 | 10 000 |
| Bind parameters (max) | 100 000 | 65 535 | 65 535 |
| NULLs in ascending order | first | last | first |
Explicit NULLS FIRST/LAST |
native | native | emulated by Hibernate |
Target table in an UPDATE/DELETE sub-query |
yes | yes | no (error 1093); such writes run key-first |
The streaming fetch size reaches the driver through the provider's ProviderSupport, which opens the stream itself:
model-query-hibernate for Hibernate. Without one, the library sets none, so the driver's default applies, which on
PostgreSQL and MySQL reads the whole result into memory, and the first stream on each EntityManagerFactory logs a
warning saying so.
Longer IN lists are split into chunks automatically. One filter with more values than the bind-parameter limit fails
with MQ1306 when the query is built, and a statement whose values only together pass it fails with MQ1307 before it
runs. Only the key lists the library builds itself, for primary-key-first paging and bulk writes, are spread over
several statements, each holding the largest power of two of keys within the limits, so Hibernate's
hibernate.query.in_clause_parameter_padding cannot push one over.
How the vendor is detected¶
ModelQueryConfig.vendor(...)ormodelquery.vendor, if set. Nothing is detected.- With
model-query-hibernateon the classpath, the Hibernate dialect. This needs no connection and is correct perEntityManagerFactory, so an application with several datasources on different databases resolves each one. - Otherwise
DatabaseMetaData#getDatabaseProductName(), read once per factory from the factory'sjakarta.persistence.nonJtaDataSourceproperty.
An unrecognised database, or a recognised one without a profile (MariaDB, Oracle, SQL Server), resolves to the
OTHER profile. It is deliberately conservative: fetch size 500, IN lists of 1 000, 2 000 bind parameters, unknown
null ordering and no target table in sub-queries. Under OTHER, keyset paging over a nullable column without explicit
null precedence is refused. If your database is one of the Tier-1 vendors but resolves to OTHER, add
model-query-hibernate or set the vendor explicitly.
H2¶
- Keep H2's default null ordering. With
DEFAULT_NULL_ORDERINGset to anything else, Hibernate's H2 dialect assumes NULLs sort smallest and leaves an explicitnullsFirst()/nullsLast()bare where it is needed. NULLs are then misplaced, and keyset paging may skip them even with explicit precedence. The library cannot detect this without a connection.
PostgreSQL¶
- Streaming needs a transaction. The PostgreSQL driver only uses a cursor when autocommit is off. Outside a
transaction,
streamfails fast withMQ2101instead of buffering the whole result. With Spring Data, the repository opens a read-only transaction for you, so this mostly guards plain-JPA code. - Text comparison is case-sensitive and follows column collation.
likeIgnoreCaselower-cases the column and the value; a database whoselower()folds only ASCII, such as PostgreSQL withCcollation, can disagree on non-ASCII text, and the filter needs a functional index to be fast. - NULLs sort last in ascending order. Keyset paging over a nullable column therefore wants an explicit
nullsFirst()ornullsLast().
MySQL¶
- Streaming default (
row-by-row). MySQL streams withfetchSize = Integer.MIN_VALUEby default. It is the fastest mode, but it holds the connection until the whole result has been read and blocks other statements on that connection until then. This is why keysetexport, which runs a short query per page, is the recommended way to read large results. If blocking the connection does not suit you, switch to cursor fetch. - Cursor fetch. Set
modelquery.mysql.streaming-mode=cursor-fetch(orModelQueryConfig.mysqlStreamingMode(MysqlStreamingMode.CURSOR_FETCH)) and adduseCursorFetch=trueto the JDBC URL. The library cannot set the URL for you: without it, Connector/J ignores the fetch size and buffers the whole result. Cursor fetch usesmodelquery.stream.fetch-sizeand does not block the connection. The mode is read once when the profile is created; it is never decided per query. - Timeouts.
jakarta.persistence.query.timeoutbecomesStatement.setQueryTimeout, which has one-second granularity, so a sub-second timeout is rounded up. Connector/J cancels a query by opening a second connection to runKILL QUERY, which costs one extra connection per cancelled query. Count this in your pool size if your timeouts are short. *_cicollations are case-insensitive, soeqmatches'a'and'A'.- NULLs sort first in ascending order, and explicit
NULLS FIRST/LASTis emulated by Hibernate with anISNULLsort key. - Bulk writes and error 1093. MySQL cannot read the table being updated in a sub-query. When a bulk write's filter
needs a join, goes through inheritance, or has an
exists(...)reading the root's table, even through another entity mapped to it, the library selects the matching keys first and writes them in chunks. A change to a joined row between the two steps is not re-checked;ChunkOptions.lockKeys()selects the keys with a pessimistic write lock, which on MySQL also reads current rows instead of the transaction's snapshot. Telling two entities on one table apart needsmodel-query-hibernate, which reports the tables each entity reads, joined supertables and secondary tables included; without it, such a write fails with error 1093.
What the library leaves to the database¶
These are not abstracted, and the library never silently emulates them:
- String comparison and case sensitivity follow the column's collation: MySQL
*_ciis case-insensitive, PostgreSQL is case-sensitive. - JSON, array and enum column types follow your entity mapping.
- Boolean storage and date/time precision follow Hibernate and the JDBC driver. Time zones follow
hibernate.jdbc.time_zone. - A default null ordering configured on the provider. Hibernate's
hibernate.order_by.default_null_orderingis only honoured withmodel-query-hibernate. Without it, the library logs a warning once at startup when it sees the setting; add the module, or give your nullable keyset columns explicit null precedence.
If a use case needs uniform behaviour across databases, ask for it explicitly: for example use eqIgnoreCase
and an index, instead of relying on a collation.