Upgrading
What each release needs from you, when it needs anything. Releases not listed here are drop-in.
2026.9.8
Converters are generated beside your repositories
converter.mapConverterClass used to default to com.example.persistence.converter.ToMapConverter,
and every generated converter is written to the package that setting names. A project that set
repositories.basePackageName and nothing else therefore had its repositories generated where it
asked and its converters generated into the example’s package — a name it had never written down,
reached through a setting it had never heard of.
The default is now the bare name ToMapConverter, and a name without a package is resolved against
repositories.basePackageName. So converters land in <your base package>.converter, and a project
that already worked around this by writing the setting out in full keeps exactly what it had — a
fully-qualified name is still followed to the letter.
Nothing to do unless you configured mapConverterClass solely to relocate the converters. That
setting can go:
<converter>
<mapConverterClass>com.example.store.converter.ToMapConverter</mapConverterClass>
</converter>
If you want the converters to stay where they are while your base package differs, keep it. Anything of your own importing a generated converter moves with it — that shows up as a compile error naming the import, not as anything silent.
A schema.sqlStatementsDirectory holding versioned migrations is now read in version order.
Previously the files were read in name order, so V10__ and V12__ were applied ahead of V2__
and every column added past the ninth migration was missing from the schema YoSQL worked from.
Nothing needs changing, but two things are worth rechecking if you pointed this at a Flyway or
Liquibase directory. Schema validation lowered to WARN because it reported columns that plainly
exist can go back to ERROR. Parameter and result types written out by hand because inference
“did not work” are now inferred, and the front matter still wins where the two disagree — so a type
declared against the wrong schema stays wrong until you delete it.
A column is also read in the spelling its own DDL was written in. Marking a schema file with a
vendor
now decides how its column types are read, so bytea,
timestamptz, bigserial, jsonb and the rest of a database’s own spellings reach statements that
declare no vendor themselves:
-- vendor: postgresql
create table attachment (id bigserial primary key, payload bytea not null);
Previously those spellings were only read when the statement declared the vendor, which a project with one database has no reason to do — so its schema answered for the standard types and nothing else. Nothing changes for a project that marks its statements, or for one that marks neither.
Whether marking the schema gains you anything depends on how your DDL is written, and it is worth
checking rather than assuming. uuid, text, varchar, boolean, integer, bigint, numeric,
date and timestamp with time zone are read the same by every database, so a schema written in
those types already types completely, and a vendor adds nothing to it. A schema saying timestamptz,
bigserial, bytea, jsonb or citext is the one this is for.
The build says which columns it cannot type
, which
answers it for your schema without guessing.
Where the schema is a migration directory, the files are checksummed by the tool that applied them
and a comment added to one already run makes it refuse to start. The new schema.vendor setting
says the same thing from the build instead:
<schema>
<sqlStatementsDirectory>src/main/resources/db/migration</sqlStatementsDirectory>
<vendor>PostgreSQL</vendor>
</schema>
A schema file naming its own vendor keeps it.
Undo migrations are no longer read as schema
A Flyway undo migration — U48__… beside the V48__… it reverses — says how to take a migration
back out. It is never run by a migration, so the database has never had it applied, and reading it
describes a schema that does not exist. It also carries no version this reader recognised, so it
sorted after every versioned migration and undid them last of all.
What that looked like was a column missing from an otherwise perfect schema: U48’s drop column
is something the reader follows exactly, so it took the column back out and reported nothing. If a
statement of yours was reported as using a column that plainly exists, and the migration adding it
has an undo beside it, that was this.
A camelCase parameter finds its snake_case column
:accountId now takes its type from an account_id column, the same conversion a result row
component has always had. Previously a parameter had to be spelled exactly as the column was, so
every parameter named after a snake_case column fell through to “no type known for” and had to be
declared by hand — the schema had the answer and the name never reached it. If your front matter is
full of parameters: blocks that only repeat what the schema says, they can go.
A parameter written the column’s way keeps working, and a type in the front matter still wins over both.
A write holding a collection no longer asks for a batch
executeBatch defaults to on for writing statements, and a collection parameter cannot be batched —
each of its values needs a placeholder of its own, so every execution would need a different query.
Those two defaults met in a build failure on every update … where state in (:states), curable only
by writing executeBatch: false on each one. The default now yields: a statement holding a
collection is generated without a batch method. Writing executeBatch: true on one is still an
error, because that asks for something that cannot exist.
Two generated types with one name stop the build
A repository interface is the repository’s name without its Repository suffix, so statements in a
document directory generate an interface called Document alongside DocumentRepository. A
resultRowType naming <your base package>.persistence.Document names that same type, and the two
were written to one file — whichever the generator reached second was the one that survived.
What that looked like was a compile error on a file you did not write, saying the type a converter instantiates is abstract, or a duplicate class where the record was your own. Now it stops generation and names both:
Two generated types would both be written as 'com.example.persistence.Document': an interface
and a record.
The same name can also meet a class you wrote yourself, which is the likelier of the two: a project
keeping its stores in the repositories’ package has hand-written classes sitting exactly where the
generator writes, and a windDown statements directory generates an interface called WindDown.
That was a duplicate class from javac, in whichever of the two files it reached first. It is now
the same kind of failure, before anything is written, naming the file it met.
Either way this only affects a build that was already failing at compile time, so nothing that worked can start failing. If you see it, rename whichever of the two names is yours, move it to another package, or turn off generateInterfaces .
A parameters: block no longer changes the order of a method
A generated method takes its parameters in the order the statement binds them. Declaring some of them used to move those to the front:
-- parameters:
-- tenantId: uuid
select id from restore where id = :id and tenant_id = :tenantId
findWithdrawableRestore(UUID tenantId, UUID id) // before
findWithdrawableRestore(UUID id, UUID tenantId) // now, and what the SQL says
Check any statement whose block names fewer parameters than the statement binds, or names them in
an order the SQL does not use. Where the moved parameters have different types, your call sites
stop compiling and the compiler shows you every one. Where they share a type — two UUIDs, as
above — they compile before and after, and the arguments were going in the wrong way round before
this release. That is the case worth looking for: a query returns nothing where it should return a
row, and a write touches the wrong row.
A block naming every parameter in the order the SQL binds them is unaffected, which is how most are written.
The build says which columns it cannot type
Alongside the table count, a run now names the columns the schema holds and cannot give a Java type:
The schema holds 1 column(s) whose type YoSQL does not map: document.payload (jsonb). No vendor
is declared, and the types only one database has are looked up only for a declared one — so
'schema.vendor' may be all that is missing.
The table count is the same whether or not a vendor is declared — it is the columns that change — so it was the wrong figure to read a vendor’s effect off. This is the one that answers it. Nothing to do: an untyped column that no statement selects and no parameter is named after costs nothing.
2026.8.8
The first release since 2023.5.3, and it changes enough to be worth reading before you bump the version.
Java 25 is now the minimum
Both to run YoSQL and to compile and run what it generates. Generated code uses var, text
blocks, records and sequenced collections.
If your project is on an older Java release, stay on 2023.5.3 until you can move. There is no
configuration that makes the generator emit older code — the switch that used to do that is gone,
because it silently produced output that no longer matched what the documentation promised.
A converter is named by its class, and nothing else
The four-part converter description and the registry that held it are gone. A statement that used to name a converter by alias:
-- resultRowConverter:
-- alias: itemConverter
now names the class:
-- resultRowConverter: com.example.persistence.converter.ToItemConverter
and the build configuration that declared the alias goes away entirely. In Maven:
<converter>
<!-- delete the whole rowConverters block -->
<rowConverters>
<rowConverter>
<alias>itemConverter</alias>
<converterType>com.example.persistence.converter.ToItemConverter</converterType>
<methodName>asUserType</methodName>
<resultType>com.example.domain.Item</resultType>
</rowConverter>
</rowConverters>
</converter>
The same applies to rowConverters in Gradle and Ant, and to --rowConverters on the command
line. defaultConverter survives but now takes a single class name rather than the four parts.
YoSQL reads the class to find its one public method taking a ResultSet. That method’s name is
what the repository calls and its return type is what the statement produces, so nothing repeats
what the class already says. Two consequences worth knowing:
- The converter has to be visible as source under sourceDirectory , because it is read rather than loaded. A converter that lives in a different module needs that directory pointed at it.
- The class must declare exactly one public method taking a
ResultSet. None, or more than one, fails the build and names the class.
The field the repository holds the converter in is now the class name with a lower-case first
letter — toItemConverter rather than whatever the alias was. That only matters if you were
reading generated fields directly.
Generic types cannot be result rows
A record declaring type parameters is refused rather than mapped, because a statement says nothing about what to substitute for them. Name a concrete type instead.
YoSQL reads your schema now
If your project keeps create table statements where YoSQL can see them, this release starts
holding the rest of your SQL to them: a column that does not exist, a parameter whose type disagrees
with its column, a nullable column read into a primitive. Nothing connects to a database, so it
works in a checkout with no services running.
It reports and does not stop. The default is WARN, so no build that passed before fails
because of this — expect new warnings, not new failures. Read them; they are the queries that would
have failed on whichever request reached them first.
Once you have dealt with them, turn it up so nothing new gets in:
<schema>
<validation>ERROR</validation>
</schema>
To hear nothing at all, set it to OFF.
It also settles what a parameter’s type is, which removes the parameters block from write
statements entirely — see schema validation
.
A colon with no name after it is no longer a parameter
:id is a parameter. A bare : is not, and used to be read as one — which made two ordinary things
wrong.
A statement carrying a licence header picked up a parameter with no name from the colon in
SPDX-License-Identifier:. And PostgreSQL’s :: cast bound two parameters, the bare colon and the
type name, so every parameter after a cast was bound to the wrong placeholder — the statement
ran and answered with the wrong rows.
If you have statements using :: casts, their generated methods change: the spurious parameters
disappear and the real ones move to the indices they should always have had. Check any such method’s
signature after upgrading, and be glad if you never hit the bug.
Every statement is now reachable with and without a connection
Where a statement got its connection used to be decided per statement by createConnection, and you
got one method. It is now decided by the caller, and you get two:
Optional<Tenant> findTenant(UUID id);
Optional<Tenant> findTenant(Connection connection, UUID id);
The first opens a connection from the repository’s DataSource and closes it; the second runs on
the one it is given. That is what lets several statements share a transaction — see
transactions
.
Nothing you call today changes name or signature, so existing code keeps compiling, with one
exception: a repository whose statements all set createConnection: false used to have a no-arg
constructor and now takes a DataSource, because it now also has methods that need one. Pass it
one, or set
generateConnectionOverloads
to
false to keep one method per statement.
If you wrote a statement twice in a file — once plain and once with createConnection: false — to
get both shapes, delete the second one. It now generates a method with a 2 in its name for no
reason.
A parameter with no type now fails instead of becoming an Object
A parameter the front matter did not type used to be bound as java.lang.Object. The method
compiled, accepted anything, and offered exactly the type safety of the JDBC it replaced — silently.
It is now a build error naming the file, the statement and every parameter still without a type.
Two things fill them in. A statement naming a record with resultRowType takes each parameter’s type from the component of the same name, so most read statements need nothing:
-- name: findTenant
-- returning: single
-- resultRowType: com.example.domain.Tenant
select id, slug from tenant where id = :id
Tenant declares UUID id, so :id is a UUID. For everything else — write statements, above all
— name the types. The front matter now takes a mapping of name to type alongside the list form:
-- parameters:
-- id: uuid
-- slug: string
-- createdAt: instant
uuid, string and instant are short names for the types statements are usually written in; the
full list is under parameters
. A fully-qualified class name
still works everywhere, and the list form is unchanged for parameters that need a sqlType, a
scale or a variant.
Where the build fails, the message shows the front matter to add. Nothing else has to change.
Statements that generate nothing now fail
A statement whose name matches none of the configured prefixes, and which sets no explicit type,
used to be skipped without a word — so a typo removed a method from your repository and said
nothing. It is now a build error naming the file and the statement. If a release starts failing
here, it is reporting something that was already broken.
The TI logging API is gone
logging.api no longer accepts TI. The generator behind it was never written: it answered every
question with an empty block while reporting that logging was off, and the repositories it produced
did not compile — a bare if () { reading a variable nothing declared.
If your build sets it — <api>TI</api> in a pom.xml, api = 'TI' in a build.gradle, or
--logging-api=TI on the command line — pick one of NONE, JUL, SYSTEM, LOG4J, SLF4J or
TINYLOG. NONE is the closest to what TI actually did, which was nothing.
The java configuration group is gone
Its six switches decided whether generated classes, fields, methods, parameters and locals were
declared final. Generated code reassigns none of them, so they all are now, which is what the
defaults already said. One of them, useSealedInterfaces, was documented in every frontend and did
nothing at all.
Remove the block if your build has one — a <java> element in a pom.xml, a java { } block in a
build.gradle, a <java> element in an Ant task, or --use-final-* on the command line. Maven and
Ant fail on an unknown element, so this one is not optional. Nothing about the generated code
changes unless you had turned one of them off, in which case the output gains the final keywords
it describes.
The names configuration group is gone
Its twenty-two settings renamed the variables inside generated methods — the Connection, the
ResultSet, the loop counter. None of them is part of a repository’s API, so none of them was a
decision worth making: a name nobody outside the method can see cannot fit a codebase better or
worse. They also had to be kept distinct from each other, which is why a whole validator existed to
check they were.
Remove the block if your build has one — a <names> element in a pom.xml, a names { } block in
a build.gradle, a <names> element in an Ant task, or the matching command-line options. Maven and
Ant fail on an unknown element, so this one is not optional. Generated code is unchanged unless you
had renamed something, in which case it goes back to the default name.
The one thing those settings could rescue was a statement whose own parameter is called connection,
statement, resultSet, index or another name a generated method already uses. That is now a
build error naming the file, the statement and the parameter, instead of a Java error about a
variable already defined in a file you did not write. Rename the parameter in the SQL — the name
reaches no further than the method’s signature.
skipLines is gone, and a licence header is dropped on its own
A .sql file that opens with a block comment has it dropped, however many lines it runs to. The
count that used to have to match — and to be kept matching every time somebody edited the header —
is no longer a setting.
Remove skipLines from your build; Maven and Ant fail on an unknown element. If your headers are
written as -- lines rather than as a block comment, rewrite them as /* … */. A -- line at the
top of a file is front matter, and -- SPDX-License-Identifier: 0BSD is as good a YAML mapping as
-- name: findTenant, so there is no way to tell the two apart.
Generated code now says so, and the annotations switches are gone
Every generated class, field and method carries @Generated:
@Generated(
value = "wtf.metio.yosql",
comments = "generated by YoSQL 2026.8.8 - do not modify, this file is rewritten on every build"
)
It used to be off unless you turned it on, which is backwards for a marker whose whole job is to tell
coverage tools, linters and the next reader that nobody wrote this by hand. Eleven settings decided
whether it appeared, which annotation class it used, which members it carried and what each of them
said; all eleven are gone, and so is the javax.annotation.Generated option, which has not existed
in the JDK since Java 11.
The value is the generator’s fully qualified name, which is what the annotation’s own
documentation asks for. The comments carry the release that wrote the file — the one fact about
generated code that is nowhere else in it, since which .sql file a method came from is already in
its javadoc.
There is deliberately no date. Two builds of the same statements produce the same code, and a timestamp would be the only thing in the output that differed between them.
javax.annotation.processing.Generated lives in the java.compiler module, which a classpath build
always has. A modular project compiling generated code inside its own module needs
requires static java.compiler; in its module-info.java — static, because the annotation is
discarded after compilation and nothing needs it at run time.
Remove the <annotations> switches from your build — annotationApi, annotateClasses,
annotateFields, annotateMethods, classMembers, fieldMembers, methodMembers,
classComment, fieldComment, methodComment and generatorName. Maven and Ant fail on an unknown
element. The three settings that add your own annotations to generated repositories,
constructors and methods are unchanged.
Repository and method names are no longer spelled out in configuration
Eight settings decided what to put around a generated name — repositoryNamePrefix,
repositoryNameSuffix, repositoryInterfacePrefix, repositoryInterfaceSuffix,
executeOncePrefix, executeOnceSuffix, executeBatchPrefix and executeBatchSuffix. Six of them
defaulted to nothing at all, and the two that did something were carrying the two names that have to
differ from each other:
- A repository is its directory plus
Repository, sotenant/*.sqlbecomesTenantRepository. - Its interface is the same name without that suffix —
Tenant— and gets anIin front only when there is no suffix to drop. - A batch method is its statement plus
Batch, soinsertTenantandinsertTenantBatchcan live in the same repository.
That is what the defaults already produced. Remove the settings from your build and from any front matter that set them per statement; Maven and Ant fail on an unknown element. If you had renamed something, the generated names change back — name the repository with repository in the front matter, which still decides it outright.
sqlStatementsDirectory is relative to your project, not to where the build was started
sqlStatementsDirectory
says where the DDL
describing your schema lives. Written as a relative path, it used to be resolved against whatever
directory the build was running in rather than against the module being built — so in a multi-module
project, mvn verify from the root looked for the schema under the root while mvn verify inside
the module found it. Every frontend resolves it against the project now, the same as every other
directory setting.
Nothing reports the difference, which is what makes it worth checking: a schema directory that resolves to nothing is indistinguishable from a schema that raises no complaints, because reading the schema is designed never to fail a build. If you set this to a relative path, confirm it is relative to the module’s own directory. An absolute path is unaffected.
Maven is where this is most likely to have bitten you. The CLI runs in the directory you invoke it from, which is the answer it now arrives at deliberately; Gradle ran in the project’s own directory, so it already found the right one, and no longer depends on that being true.
Ant can set the method name prefixes and the annotations
Ant builds an attribute setter only for a type it can make out of a string, and
allowedCallPrefixes
,
allowedReadPrefixes
and
allowedWritePrefixes
are lists — so the
task declared them and then refused them, with doesn't support the "allowedReadPrefixes" attribute. The whole annotations
group went the same way: the
nested element could be written, and nothing inside it could be set.
Both work now. The three lists are one attribute, separated by commas:
<repositories validateMethodNamePrefixes="true"
allowedReadPrefixes="fetch,find"/>
An annotation is a nested element, and each of its members is a nested element of that:
<annotations>
<repositoryAnnotations type="jakarta.inject.Named">
<member key="value" value="tenants"/>
</repositoryAnnotations>
</annotations>
Nothing here can have broken a build: none of it could be set before. If you turned
validateMethodNamePrefixes off because the prefix lists were out of reach, it is worth turning
back on.
2023.5.3 and earlier
See the release notes for those versions.