Skip to content

feat: enforce exact and optional cardinality and name failing queries - #28

Merged
omer-cengel merged 2 commits into
masterfrom
feature/exact-and-optional-query-cardinality
Sep 24, 2026
Merged

omer-cengel merged 2 commits into
masterfrom
feature/exact-and-optional-query-cardinality

Conversation

@omer-cengel

Copy link
Copy Markdown
Member

Summary

A :one query no longer returns null when no row matches or silently returns the first of several rows. It now returns exactly one row or fails with a focused QueryCardinalityException, and the new :optional annotation covers zero-or-one lookups by returning Optional<T>. Every runtime execution and cardinality failure names the generated repository and query, so the application can tell which call failed.

Changes

  • :one returns its row directly and raises dev.sqlcj.runtime.QueryCardinalityException (a QueryExecutionException subclass) when the query returns no row or more than one.
  • New :optional annotation, accepted for a SELECT or a write with RETURNING wherever :one is. It returns Optional.empty() for no row, Optional.of(row) for one, and raises the same exception for more than one.
  • :many still returns List<T> (empty when no row matches) and :exec still returns the affected-row count.
  • QueryExecutor now exposes queryOne, queryOptional, queryMany, and execute, each taking the repository class name and query name. Generated methods pass both as string literals.
  • Failure messages read, for example, Query 'GetAuthor' in AuthorRepository returned no row; expected exactly one and Failed to execute query 'GetAuthor' in AuthorRepository. Statements, result sets, and acquired connections are closed on a cardinality failure.
  • Generated repositories import java.util.Optional only when they declare an :optional query. Full-row :optional queries share the table's <TableName>Row record.
  • The external Maven sample adds an optional FindAuthor lookup in place of its null missing-row checks and shows the :one failure, and release verification requires the new method.
  • The query, PostgreSQL, and quickstart guides document the four annotations, the exception messages, and that a result component is null only for a nullable column while row absence is expressed only by the annotation.

Scope and non-goals

  • A cardinality check runs after the statement executes, so a returning write that fails it has already changed the database; transaction control stays with the caller-owned connection.
  • Scalar Java type mappings are unchanged. Record components and parameters are not wrapped in Optional, and no nullness annotations or new nullness type system are added.
  • Parameter order, JDBC binding order, and selected-column order are unchanged.
  • No new SQL syntax is supported.

A :one query now returns exactly one row and raises
QueryCardinalityException, a QueryExecutionException subclass, when it
returns no row or more than one, instead of returning null or the first
row. The new :optional annotation is accepted wherever :one is and
returns Optional<T>, empty when no row matches, failing when more than
one row is returned. :many and :exec are unchanged.

QueryExecutor now exposes queryOne, queryOptional, queryMany, and
execute, each taking the generated repository class name and query name,
and every execution and cardinality failure message names them.
Generated code imports java.util.Optional only for an :optional query.
The external sample gains an optional FindAuthor lookup in place of its
null missing-row checks, and release verification requires it.
@omer-cengel
omer-cengel merged commit 8b324a5 into master Sep 24, 2026
6 checks passed
@omer-cengel
omer-cengel deleted the feature/exact-and-optional-query-cardinality branch September 24, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant