Skip to content

Commit 54bcb07

Browse files
authored
Merge pull request #55 from sqlcj-dev/feature/named-query-placeholders
feat: support named :name query placeholders
2 parents 9ade6ca + 22a6679 commit 54bcb07

11 files changed

Lines changed: 1133 additions & 252 deletions

File tree

‎docs/queries.md‎

Lines changed: 88 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ after `code` and disambiguated as `code1` and `code2`. The bounds are bound in
171171
textual order, the start bound before the end bound, whatever their placeholder
172172
indexes are. A placeholder bound beside a literal bound, as in
173173
`code BETWEEN $1 AND 10`, is typed the same way and binds one parameter. A named
174-
bound such as `code BETWEEN :lo AND :hi` is rejected like any other named
175-
placeholder, while a placeholder as the tested value, as in
174+
bound such as `code BETWEEN :lo AND :hi` is typed the same way and named after
175+
its placeholder, while a placeholder as the tested value, as in
176176
`$1 BETWEEN code AND code`, and a placeholder inside a computed bound, as in
177177
`code BETWEEN $1 + 1 AND $2`, are rejected as unanalyzed placeholder locations.
178178

@@ -213,8 +213,10 @@ LIMIT $1 OFFSET $2;
213213
parameter.
214214
- Both values must be non-negative. sqlcj generates no validation, so PostgreSQL
215215
rejects a negative value when the query executes.
216-
- A named value such as `LIMIT :n` or `OFFSET :n` is rejected like any other
217-
named placeholder. A placeholder in a computed value, as in `LIMIT $1 + 1` or
216+
- A named value is named after its placeholder rather than after its clause, so
217+
`LIMIT :pageSize OFFSET :skip` generates
218+
`listAuthorPage(Integer pageSize, Integer skip)`.
219+
- A placeholder in a computed value, as in `LIMIT $1 + 1` or
218220
`OFFSET $1 + 1`, in the `LIMIT a, b` form, as in `LIMIT 5, $1`, and in a
219221
`FETCH FIRST $1 ROWS ONLY` clause are rejected as unanalyzed placeholder
220222
locations.
@@ -225,8 +227,8 @@ A write targets exactly one table of its entry's schema.
225227

226228
| Statement | Accepted shape |
227229
| --- | --- |
228-
| `INSERT` | an explicit column list and a single `VALUES` row whose values are all `$N` placeholders |
229-
| `UPDATE` | `SET` assignments that each assign one direct column a single `$N` placeholder, with an optional `WHERE` using the read predicate forms |
230+
| `INSERT` | an explicit column list and a single `VALUES` row whose values are all placeholders |
231+
| `UPDATE` | `SET` assignments that each assign one direct column a single placeholder, with an optional `WHERE` using the read predicate forms |
230232
| `DELETE` | one target table with an optional `WHERE` using the read predicate forms |
231233

232234
```sql
@@ -268,24 +270,40 @@ table expressions are rejected in a returning write.
268270

269271
## Parameters
270272

271-
sqlcj uses PostgreSQL `$N` placeholders. The compiler replaces each real
272-
placeholder token with a JDBC `?` and leaves every other character of the
273-
statement byte-for-byte unchanged, so `$1` inside a string literal, a quoted
274-
identifier, or a comment is not a parameter.
273+
A query writes its parameters either as PostgreSQL `$N` placeholders or as
274+
`:name` placeholders, and one query uses one of the two forms. The compiler
275+
replaces each real placeholder token with a JDBC `?` and leaves every other
276+
character of the statement byte-for-byte unchanged, so `$1` or `:id` inside a
277+
string literal, a quoted identifier, or a comment is not a parameter.
275278

276279
- Placeholder indexes must be positive and contiguous from `$1`.
280+
- A named placeholder is a colon followed directly by an unquoted name of ASCII
281+
letters, digits, and underscores that does not start with a digit, such as
282+
`:userId`. Names are compared exactly as written, so `:term` and `:Term` are
283+
two parameters, and a name spelled like a SQL keyword, such as `:limit`,
284+
`:user`, or `:year`, is an ordinary name.
285+
- Each distinct name is one parameter, numbered by its first textual
286+
occurrence, and every occurrence of that name is bound at its own `?`
287+
position.
288+
- A named placeholder is accepted wherever a `$N` placeholder is, and names its
289+
generated method parameter after itself, so `LIMIT :pageSize` generates
290+
`pageSize` rather than `limit`.
277291
- The Java type of a placeholder is the type of the column it is compared with,
278292
assigned to, or inserted into.
279-
- Anonymous `?` placeholders and named `:name` placeholders are rejected.
293+
- Mixing `$N` and `:name` placeholders in one query is rejected, as are
294+
anonymous `?` placeholders, a qualified name such as `:a.b`, a quoted name
295+
such as `:"x"`, and an `&name` placeholder.
280296
- A placeholder in a location sqlcj does not analyze — for example `ORDER BY $1`
281-
— is rejected rather than left unbound.
297+
or `lower(name) = :name` — is rejected rather than left unbound.
282298

283299
### Logical order versus textual order
284300

285301
The two orders are distinct and both are observable:
286302

287303
- **Logical order** is placeholder index order. It is the order of the generated
288-
method parameters: `$1` is the first method parameter, `$2` the second.
304+
method parameters: `$1` is the first method parameter, `$2` the second. In a
305+
named query it is first-occurrence order: the name written first is the first
306+
method parameter.
289307
- **Textual order** is the order in which placeholder tokens appear in the SQL.
290308
It is the JDBC binding order of the generated `?` positions.
291309

@@ -308,11 +326,24 @@ public int updateAuthorBio(Long id, String bio) {
308326
}
309327
```
310328

311-
An index may repeat. A repeated index produces one method parameter, named and
312-
typed from its first occurrence, and its value is bound at every textual
313-
position where the index occurs. Occurrences of one index whose inferred Java
329+
An index and a name may repeat. A repeated index produces one method parameter,
330+
named and typed from its first occurrence, and its value is bound at every
331+
textual position where the index occurs; a repeated name behaves the same way
332+
and keeps its own name. Occurrences of one index or one name whose inferred Java
314333
types differ are rejected.
315334

335+
`UpdateAuthorBio` written with named placeholders states the same two orders:
336+
337+
```sql
338+
-- name: UpdateAuthorBio :exec
339+
UPDATE authors
340+
SET bio = :bio
341+
WHERE id = :id;
342+
```
343+
344+
The generated method takes `(bio, id)`, because `:bio` occurs first, and binds
345+
`(bio, id)`.
346+
316347
## Generated Java
317348

318349
Each configuration entry generates one final repository class in the configured
@@ -531,7 +562,9 @@ name, and its header line.
531562
is not a plain inner or left join, a join predicate that is not one qualified
532563
equality, and a set operation such as `UNION`.
533564
- A placeholder in a location sqlcj does not analyze, including `ORDER BY $1`, a
534-
computed `LIKE` pattern such as `'%' || $1 || '%'`, a placeholder as the
565+
named placeholder under a function such as `lower(name) = :name` or under a
566+
cast such as `:name::text`, a computed `LIKE` pattern such as
567+
`'%' || $1 || '%'`, a placeholder as the
535568
tested value of a range such as `$1 BETWEEN id AND id`, a computed range
536569
bound such as `id BETWEEN $1 + 1 AND $2`, a computed pagination value such as
537570
`LIMIT $1 + 1` or `OFFSET $1 + 1`, a `LIMIT a, b` row count such as
@@ -540,11 +573,10 @@ name, and its header line.
540573
- A `LIKE`-family pattern placeholder that is negated, uses another keyword such
541574
as `SIMILAR TO`, carries an `ESCAPE` clause or a `BINARY` modifier, tests a
542575
non-text column, or stands as the tested value.
543-
- A named range bound such as `id BETWEEN :lo AND :hi` and a named pagination
544-
value such as `LIMIT :n` or `OFFSET :n`, which fail with the named-placeholder
545-
diagnostic.
546-
- Anonymous `?` and named `:name` placeholders, and non-contiguous or
547-
non-positive placeholder indexes.
576+
- Anonymous `?` placeholders, `$N` and `:name` placeholders mixed in one query,
577+
a qualified name such as `:a.b`, a quoted name such as `:"x"`, an `&name`
578+
placeholder, one name whose occurrences have conflicting types, and
579+
non-contiguous or non-positive placeholder indexes.
548580
- An `INSERT` without an explicit column list, with more than one `VALUES` row,
549581
with a value that is not a placeholder, or built from a `SELECT`; and an
550582
`UPDATE` assignment that is not a single placeholder.
@@ -569,9 +601,9 @@ them:
569601
- `ON CONFLICT`, `UPDATE ... FROM`, and `DELETE ... USING` on a non-returning
570602
`:exec` write.
571603

572-
sqlcj itself provides no named parameters, macros, array operators such as
573-
`= ANY`, dynamic `IN` expansion, or query-building API. An array parameter is
574-
one whole list bound at one placeholder, not a placeholder list.
604+
sqlcj itself provides no macros, array operators such as `= ANY`, dynamic `IN`
605+
expansion, or query-building API. An array parameter is one whole list bound at
606+
one placeholder, not a placeholder list.
575607

576608
Unsupported schema input and unsupported column types are listed in
577609
[PostgreSQL Support](postgresql.md#unsupported-types-and-ddl).
@@ -659,16 +691,16 @@ Reads:
659691
`QueryAnalyzerTest.shouldResolveRangeBoundParameterBesideLiteralBound`,
660692
`QueryAnalyzerTest.shouldNotCreateParameterForLiteralRange`,
661693
`QueryAnalyzerTest.shouldRejectUnknownColumnInRangePredicate`, and
662-
`QueryAnalyzerTest.shouldRejectNamedRangeBound` cover the range predicates,
663-
and
694+
`QueryAnalyzerTest.shouldResolveNamedRangeBoundParameters` cover the range
695+
predicates, and
664696
`PostgresIntegrationTest.shouldExecuteGeneratedRangePredicatesAgainstPostgres`
665697
executes a `BETWEEN` read and a `NOT BETWEEN` read whose bounds use
666698
out-of-order placeholder indexes against PostgreSQL 16.
667699
- `QueryAnalyzerTest.shouldResolvePaginationParametersAfterPredicateParameters`,
668700
`QueryAnalyzerTest.shouldResolvePaginationParametersInTextualBindingOrder`,
669701
`QueryAnalyzerTest.shouldResolveOffsetParameterBesideUnanalyzedRowCount`,
670702
`QueryAnalyzerTest.shouldNotCreateParametersForLiteralPagination`,
671-
`QueryAnalyzerTest.shouldRejectNamedPaginationValue`, and
703+
`QueryAnalyzerTest.shouldNameNamedPaginationParameterAfterItsPlaceholder`, and
672704
`QueryAnalyzerTest.shouldRejectPlaceholderInUnsupportedPaginationValue` cover
673705
pagination, and
674706
`PostgresIntegrationTest.shouldExecuteGeneratedPaginationAgainstPostgres`
@@ -731,10 +763,36 @@ Parameters:
731763
`QueryAnalyzerTest.shouldRejectGappedParameterIndexes`,
732764
`QueryAnalyzerTest.shouldRejectZeroParameterIndex`,
733765
`QueryAnalyzerTest.shouldRejectPlaceholderInUnsupportedLocation`,
734-
`QueryAnalyzerTest.shouldRejectAnonymousParameter`,
735-
`QueryAnalyzerTest.shouldRejectNamedParameter`, and
766+
`QueryAnalyzerTest.shouldRejectAnonymousParameter`, and
736767
`QueryAnalyzerTest.shouldKeepPlaceholderTextThatIsNotAParameter` cover
737768
ordering, repetition, and rejection.
769+
- `SqlParserTest.shouldCompileNamedParametersByFirstOccurrence`,
770+
`SqlParserTest.shouldCompileNamesThatDifferInCaseAsDistinctParameters`,
771+
`SqlParserTest.shouldReportBothPlaceholderFormsOfOneSource`,
772+
`SqlParserTest.shouldNotCompileUnsupportedNamedPlaceholderForms`,
773+
`SqlParserTest.shouldPreserveNamedPlaceholderTextThatIsNotAParameter`,
774+
`SqlParameterCompilerTest.shouldReplaceReportedNamedParameterSpans`,
775+
`SqlParameterCompilerTest.shouldIgnoreNameSeparatedFromItsColon`, and
776+
`SqlParameterCompilerTest.shouldRejectNamedSpanThatDoesNotHoldTheParameterImage`
777+
cover the compiled named form, its numbering, and its span guards.
778+
- `QueryAnalyzerTest.shouldResolveNamedParameter`,
779+
`QueryAnalyzerTest.shouldNumberNamedParametersByFirstOccurrence`,
780+
`QueryAnalyzerTest.shouldResolveNamedParametersOfUpdate`,
781+
`QueryAnalyzerTest.shouldResolveNamedParametersOfInsert`,
782+
`QueryAnalyzerTest.shouldResolveNamedParametersInInList`,
783+
`QueryAnalyzerTest.shouldResolveNamedLikePatternParameter`,
784+
`QueryAnalyzerTest.shouldResolveNamedParametersSpelledLikeKeywords`,
785+
`QueryAnalyzerTest.shouldRejectMixedPlaceholderForms`,
786+
`QueryAnalyzerTest.shouldRejectUnsupportedNamedPlaceholderForm`,
787+
`QueryAnalyzerTest.shouldRejectNamedPlaceholderInUnanalyzedLocation`, and
788+
`QueryAnalyzerTest.shouldRejectNamedPlaceholderWithConflictingTypes` cover the
789+
analyzed named locations, the generated parameter names, and the named
790+
rejections.
791+
- `SqlcjCompilerIntegrationTest.shouldExecuteGeneratedQueryWithNamedPlaceholders`
792+
and
793+
`PostgresIntegrationTest.shouldExecuteGeneratedNamedPlaceholdersAgainstPostgres`
794+
compile and execute a named query that repeats a name and orders its
795+
parameters by first occurrence.
738796
- `SqlcjCompilerIntegrationTest.shouldExecuteGeneratedQueryWithOutOfOrderPlaceholders`,
739797
`SqlcjCompilerIntegrationTest.shouldExecuteGeneratedQueryWithRepeatedPlaceholder`,
740798
`SqlcjCompilerIntegrationTest.shouldExecuteGeneratedUpdateWithOutOfOrderPlaceholders`,

0 commit comments

Comments
 (0)