Skip to content

Commit e110322

Browse files
authored
Merge pull request #57 from sqlcj-dev/feature/cast-typed-placeholders
feat: type query placeholders by their SQL cast
2 parents b21b9ad + 9035e45 commit e110322

9 files changed

Lines changed: 1287 additions & 249 deletions

File tree

‎docs/queries.md‎

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,38 @@ its placeholder, while a placeholder as the tested value, as in
179179
A comparison that binds no placeholder, such as `active = TRUE`, contributes no
180180
generated parameter and reaches the database as written.
181181

182+
A placeholder written as the direct operand of `::type` or `CAST(... AS type)`
183+
is typed by that cast instead of by the clause it appears in, anywhere inside
184+
the `WHERE` clause: beside a compared column, as the tested value of `IS NULL`,
185+
inside a concatenation, as a function argument, as an `IN` element, as a range
186+
bound, as a pattern, or as the operand of another operator such as the array
187+
overlap `&&`. This makes the optional filter and the computed pattern idioms
188+
compile:
189+
190+
```sql
191+
-- name: ListUsersByName :many
192+
SELECT id, name
193+
FROM users
194+
WHERE (:name::text IS NULL OR name = :name)
195+
ORDER BY id;
196+
197+
-- name: SearchUsers :many
198+
SELECT id, name
199+
FROM users
200+
WHERE name LIKE '%' || :term::text || '%';
201+
```
202+
203+
`ListUsersByName` generates `listUsersByName(String name)`, which returns every
204+
row for a null argument, and `SearchUsers` generates
205+
`searchUsers(String term)`. The cast itself reaches the database as written. A
206+
cast pattern states its own type, so the pattern restrictions above apply only
207+
to an uncast pattern: `name NOT LIKE $1::text`, `name SIMILAR TO $1::text`, and
208+
`name LIKE $1::text ESCAPE '!'` are accepted and typed `text`, whatever the
209+
tested column's type is. Such a pattern is named after its placeholder rather
210+
than after the tested column, so an indexed one is `param<N>`, as in `param1`
211+
for `$1`. [Parameters](#parameters) states the accepted cast types and the name
212+
a cast placeholder takes.
213+
182214
### Ordering
183215

184216
`ORDER BY` over direct columns is supported for a stable list order, as in
@@ -272,6 +304,22 @@ value binds no placeholder, such as
272304
`UPDATE authors SET version = version + 1`, generates a method without
273305
parameters.
274306

307+
An `INSERT` value or an `UPDATE` assignment may also compute a value from a cast
308+
placeholder, which is typed by its cast wherever it appears inside the value:
309+
310+
```sql
311+
-- name: UpdateAuthorBioOrKeep :exec
312+
UPDATE authors
313+
SET bio = COALESCE(:bio::text, bio)
314+
WHERE id = :id;
315+
```
316+
317+
`UpdateAuthorBioOrKeep` generates `updateAuthorBioOrKeep(String bio, Long id)`
318+
and keeps the assignment bound before the predicate. A cast placeholder that is
319+
the whole value, as in `SET bio = :bio::text`, is typed by its cast as well and
320+
named after its column. A value that contains an uncast placeholder, such as
321+
`COALESCE($2, bio)`, stays rejected.
322+
275323
### `RETURNING`
276324

277325
A supported `INSERT`, `UPDATE`, or `DELETE` declared `:one`, `:optional`, or
@@ -314,6 +362,26 @@ string literal, a quoted identifier, or a comment is not a parameter.
314362
`pageSize` rather than `limit`.
315363
- The Java type of a placeholder is the type of the column it is compared with,
316364
assigned to, or inserted into.
365+
- A placeholder written as the direct operand of `::type` or
366+
`CAST(... AS type)` is typed by that cast instead, wherever it appears inside
367+
a `WHERE` clause, an `INSERT` value, or an `UPDATE` assignment. The cast type
368+
may be any type a column may declare and sqlcj maps, including a declared
369+
enum name, written unquoted and matched case-insensitively, and a
370+
one-dimensional array, which binds a `List`. A cast type sqlcj does not map,
371+
such as `INTERVAL` or a multi-dimensional `INT[][]`, is rejected naming the
372+
placeholder and the type.
373+
- A named cast placeholder keeps its own name. An indexed cast placeholder
374+
keeps the name of the column whose value it is — a compared column, the
375+
tested column of `IN` or `BETWEEN`, the tested column of a plain `LIKE` or
376+
`ILIKE` pattern, which is the shape that accepts an uncast pattern, or an
377+
inserted or assigned column — so `name = $1::text` names `name` as
378+
`name = $1` does. Every other indexed cast placeholder is named `param<N>`
379+
after its own index, as in `param1` for `$1`, and colliding names take the
380+
usual numeric suffix.
381+
- Occurrences of one placeholder may mix a cast and an uncast location, and the
382+
parameter keeps the name and type of its first occurrence, so
383+
`(:name::text IS NULL OR name = :name)` is one `String` parameter named
384+
`name`.
317385
- Mixing `$N` and `:name` placeholders in one query is rejected, as are
318386
anonymous `?` placeholders, a qualified name such as `:a.b`, a quoted name
319387
such as `:"x"`, and an `&name` placeholder.
@@ -586,22 +654,30 @@ name, and its header line.
586654
is not a plain inner or left join, a join predicate that is not one qualified
587655
equality, and a set operation such as `UNION`.
588656
- A placeholder in a location sqlcj does not analyze, including `ORDER BY $1`, a
589-
named placeholder under a function such as `lower(name) = :name` or under a
590-
cast such as `:name::text`, a computed `LIKE` pattern such as
657+
named placeholder under a function such as `lower(name) = :name`, a computed
658+
`LIKE` pattern such as
591659
`'%' || $1 || '%'`, a placeholder as the
592660
tested value of a range such as `$1 BETWEEN id AND id`, a computed range
593661
bound such as `id BETWEEN $1 + 1 AND $2`, a computed pagination value such as
594662
`LIMIT $1 + 1` or `OFFSET $1 + 1`, a `LIMIT a, b` row count such as
595663
`LIMIT 5, $1`, a placeholder inside a write value such as
596664
`COALESCE($2, bio)`, and a `FETCH FIRST $1 ROWS ONLY` clause, so dynamic `IN`
597-
expansion is unavailable.
665+
expansion is unavailable. A cast does not widen these locations: a cast
666+
placeholder in a projection, `ORDER BY`, a join condition, or a pagination
667+
value, such as `ORDER BY $1::int`, stays rejected, as does a placeholder that
668+
is not the direct operand of its cast, such as `(:x)::int`.
669+
- A cast type sqlcj does not map, such as `$1::interval` or a
670+
multi-dimensional `$1::int[][]`, which is rejected naming the placeholder and
671+
the declared type.
598672
- A `LIKE`-family pattern placeholder that is negated, uses another keyword such
599673
as `SIMILAR TO`, carries an `ESCAPE` clause or a `BINARY` modifier, tests a
600-
non-text column, or stands as the tested value.
674+
non-text column, or stands as the tested value. These restrict the uncast
675+
pattern placeholder; a cast pattern states its own type.
601676
- Anonymous `?` placeholders, `$N` and `:name` placeholders mixed in one query,
602677
a qualified name such as `:a.b`, a quoted name such as `:"x"`, an `&name`
603-
placeholder, one name whose occurrences have conflicting types, and
604-
non-contiguous or non-positive placeholder indexes.
678+
placeholder, each of them also as the operand of a cast, such as
679+
`:"x"::text`, `&x::text`, or `:a.b::text`, one name whose occurrences have
680+
conflicting types, and non-contiguous or non-positive placeholder indexes.
605681
- An `INSERT` without an explicit column list, with more than one `VALUES` row,
606682
or built from a `SELECT`; an `UPDATE` assignment that sets a column list, such
607683
as `SET (name, active) = ('a', TRUE)`; and a written column that the table does
@@ -841,6 +917,35 @@ Parameters:
841917
and
842918
`SqlcjCompilerIntegrationTest.shouldExecuteGeneratedQueryWithProtectedPlaceholderText`
843919
execute the compiled binding order.
920+
- `QueryAnalyzerTest.shouldResolveNamedCastParameterOfOptionalFilter`,
921+
`QueryAnalyzerTest.shouldResolveNamedCastParameterInsideComputedLikePattern`,
922+
`QueryAnalyzerTest.shouldResolveNamedCastParameterInsideUpdateAssignment`,
923+
`QueryAnalyzerTest.shouldResolveCastKeywordParameterNamedAfterItsComparedColumn`,
924+
`QueryAnalyzerTest.shouldNameCastParameterAfterItsColumn`,
925+
`QueryAnalyzerTest.shouldNameCastPatternAfterItsPlaceholderInAnUntypedPatternShape`,
926+
`QueryAnalyzerTest.shouldNameNamedCastPatternAfterItsPlaceholder`,
927+
`QueryAnalyzerTest.shouldResolveEnumCastParameter`,
928+
`QueryAnalyzerTest.shouldResolveArrayCastParameterNamedAfterItsPlaceholder`,
929+
`QueryAnalyzerTest.shouldResolveBlankPaddedCastParameter`,
930+
`QueryAnalyzerTest.shouldNameIndexedCastParameterAfterItsIndex`, and
931+
`QueryAnalyzerTest.shouldResolveCastParametersOfInsertValues` cover the cast
932+
types, the analyzed cast locations, and the name a cast placeholder takes,
933+
while `QueryAnalyzerTest.shouldRejectUnsupportedCastType`,
934+
`QueryAnalyzerTest.shouldRejectCastOccurrenceWithConflictingType`,
935+
`QueryAnalyzerTest.shouldRejectCastPlaceholderInUnanalyzedLocation`, and the
936+
cast rows of
937+
`QueryAnalyzerTest.shouldRejectUnsupportedNamedPlaceholderForm` cover the
938+
cast rejections.
939+
- `SqlParameterCompilerTest.shouldReportNamedParametersThatAreCastOperands` and
940+
`SqlParameterCompilerTest.shouldNotReportCompiledNamedParameterThatIsACastOperand`
941+
cover the cast operand the parser reports without a parse-tree node of its
942+
own.
943+
- `SqlcjCompilerIntegrationTest.shouldGenerateCompilableJavaForCastPlaceholders`
944+
compiles a repository whose cast and uncast placeholders of one column
945+
generate two disambiguated parameters, and
946+
`PostgresIntegrationTest.shouldExecuteGeneratedCastTypedOptionalFilterAgainstPostgres`
947+
executes a named cast-typed optional filter against PostgreSQL 16 with a null
948+
and a non-null argument.
844949

845950
Generated Java:
846951

0 commit comments

Comments
 (0)