@@ -104,22 +104,27 @@ configuration entry.
104104 positional row mapper.
105105- An unqualified column must be found in exactly one source; an ambiguous or
106106 unknown column is rejected.
107- - A result expression that is not a direct column, a wildcard, or the scalar
108- count below — such as another function call, an arithmetic expression, or a
109- literal — is rejected.
107+ - A result expression that is not a direct column, a wildcard, the scalar count
108+ below, or the cast projection below — such as another function call, an
109+ arithmetic expression, or a literal — is rejected.
110110- An explicit alias names the result column, so ` SELECT id AS author_id `
111111 generates the record component ` authorId ` while the component's type still
112112 comes from the column and its nullability from the column and its source.
113113 sqlcj itself resolves an alias only in the projection; every other clause
114114 reaches the database as written.
115115
116- A read may count its matching rows with an aliased ` COUNT(*) ` as its only
117- projection:
116+ A read may count its matching rows with an aliased ` COUNT(*) ` :
118117
119118``` sql
120119-- name: CountAuthors :one
121120SELECT COUNT (* ) AS total
122121FROM authors;
122+
123+ -- name: CountAuthorsByCountry :many
124+ SELECT country, COUNT (* ) AS total
125+ FROM authors
126+ GROUP BY country
127+ ORDER BY country;
123128```
124129
125130- The alias is required and may be written with or without ` AS ` , so both
@@ -128,16 +133,47 @@ FROM authors;
128133- The component is a non-null ` Long ` : the count is typed ` BIGINT ` because
129134 ` count(*) ` returns ` bigint ` , and it is never null because a count is ` 0 ` when
130135 no row matches.
136+ - The count is one result column in its own position, so it may stand beside
137+ direct columns, a wildcard, a cast projection, or another count, in either
138+ order. ` GROUP BY ` itself is not analyzed and reaches the database as written,
139+ so PostgreSQL rather than sqlcj checks that the grouping is valid.
131140- The count is accepted over any supported source, predicate, ordering, and
132141 pagination shape, and under any annotation.
133142- ` COUNT(*) ` without an alias is rejected with
134- ` COUNT(*) requires a result alias, such as COUNT(*) AS total. ` , and a
135- ` COUNT(*) ` beside another projection item is rejected with
136- ` COUNT(*) must be the only SELECT item. `
143+ ` COUNT(*) requires a result alias, such as COUNT(*) AS total. `
137144- Every other count is still an unsupported result expression, including
138145 ` COUNT(column) ` , ` COUNT(DISTINCT column) ` , ` COUNT(t.*) ` , a qualified
139146 ` pg_catalog.count(*) ` , and the ` FILTER ` and ` OVER ` forms.
140147
148+ A read may also project a computed value by stating its type in a cast:
149+
150+ ``` sql
151+ -- name: SumRoyaltiesByAuthor :one
152+ SELECT SUM (amount)::numeric AS total
153+ FROM royalties
154+ WHERE author_id = $1 ;
155+ ```
156+
157+ - Both cast spellings are the same projection, so
158+ ` SUM(amount)::numeric AS total ` and ` CAST(SUM(amount) AS numeric) AS total `
159+ are equivalent.
160+ - The alias is required and may be written with or without ` AS ` , so
161+ ` SUM(amount)::numeric total ` is equivalent as well. A cast projection without
162+ an alias is rejected with
163+ ` A cast projection requires a result alias, such as SUM(amount)::numeric AS total. `
164+ - The result column takes the type the cast states, mapped exactly as a schema
165+ column's declared type is, so a cast may name a declared enum type or a
166+ one-dimensional array such as ` ::text[] ` . A cast type sqlcj does not map is
167+ rejected with
168+ ` Result column 'total' has unsupported cast type INTERVAL ` , naming the result
169+ column and the declared type.
170+ - The component is always nullable, because the cast operand is not analyzed
171+ and so whether it can read as ` NULL ` is unknown at compile time.
172+ - The operand itself is not analyzed and reaches the database as written, so
173+ PostgreSQL rather than sqlcj checks its column references and its functions.
174+ A placeholder in a projection is still rejected, including a cast placeholder
175+ such as ` $1::int AS x ` .
176+
141177### Predicates
142178
143179A ` WHERE ` clause may combine:
@@ -644,12 +680,14 @@ the documented shapes are contract, tested, and safe to rely on.
644680Rejection stops the run with a diagnostic naming the query source, the query
645681name, and its header line.
646682
647- - A result expression that is not a direct column, a wildcard, or an aliased
648- sole ` COUNT(*) ` , including another function call, another aggregate, and a
649- literal. A ` COUNT(*) ` without an alias and a ` COUNT(*) ` beside another
650- projection item each fail with their own diagnostic, while ` COUNT(column) ` ,
651- ` COUNT(DISTINCT column) ` , ` COUNT(t.*) ` , ` pg_catalog.count(*) ` , and the
652- ` FILTER ` and ` OVER ` forms keep the unsupported-expression rejection.
683+ - A result expression that is not a direct column, a wildcard, an aliased
684+ ` COUNT(*) ` , or an aliased cast, including another function call, another
685+ aggregate, and a literal. A ` COUNT(*) ` without an alias, a cast projection
686+ without an alias, and a cast projection whose type sqlcj does not map each
687+ fail with their own diagnostic, while ` COUNT(column) ` ,
688+ ` COUNT(DISTINCT column) ` , ` COUNT(t.*) ` , ` pg_catalog.count(*) ` ,
689+ ` lower(name) AS n ` , and the ` FILTER ` and ` OVER ` forms keep the
690+ unsupported-expression rejection.
653691- A ` FROM ` item that is not a table, a comma-separated source list, a join that
654692 is not a plain inner or left join, a join predicate that is not one qualified
655693 equality, and a set operation such as ` UNION ` .
@@ -668,7 +706,8 @@ name, and its header line.
668706 is not the direct operand of its cast, such as ` (:x)::int ` .
669707- A cast type sqlcj does not map, such as ` $1::interval ` or a
670708 multi-dimensional ` $1::int[][] ` , which is rejected naming the placeholder and
671- the declared type.
709+ the declared type, or the result column and the declared type when the cast
710+ is a projection such as ` id::interval AS i ` .
672711- A ` LIKE ` -family pattern placeholder that is negated, uses another keyword such
673712 as ` SIMILAR TO ` , carries an ` ESCAPE ` clause or a ` BINARY ` modifier, tests a
674713 non-text column, or stands as the tested value. These restrict the uncast
@@ -809,13 +848,31 @@ Reads:
809848 executes a ` LIMIT ... OFFSET ... ` page and the same page written as
810849 ` OFFSET ... LIMIT ... ` against PostgreSQL 16.
811850- ` QueryAnalyzerTest.shouldResolveScalarCountColumnFromItsAlias ` ,
812- ` QueryAnalyzerTest.shouldResolveScalarCountBesidePredicateParameter ` , and
851+ ` QueryAnalyzerTest.shouldResolveScalarCountBesidePredicateParameter ` ,
852+ ` QueryAnalyzerTest.shouldResolveScalarCountBesideDirectColumn ` ,
853+ ` QueryAnalyzerTest.shouldResolveScalarCountBeforeDirectColumn ` ,
854+ ` QueryAnalyzerTest.shouldResolveGroupedCountBesidePredicateParameter ` , and
813855 ` QueryAnalyzerTest.shouldRejectUnsupportedCountProjectionForm ` cover the
814- aliased sole ` COUNT(*) ` , its focused diagnostics, and the count and function
815- forms that stay rejected, and
856+ aliased ` COUNT(*) ` alone and beside a grouped column in either position, its
857+ focused diagnostic, and the count and function forms that stay rejected, and
816858 ` PostgresIntegrationTest.shouldExecuteGeneratedScalarCountAgainstPostgres `
817859 compiles a ` :one ` count into a result record with one ` Long ` component and
818- executes it against PostgreSQL 16 for a matching and a non-matching pattern.
860+ executes it against PostgreSQL 16 for a matching and a non-matching pattern,
861+ while
862+ ` PostgresIntegrationTest.shouldExecuteGeneratedGroupedCountAgainstPostgres `
863+ executes a ` :many ` grouped count whose result record carries the grouped
864+ ` Boolean ` column and the ` Long ` count in projection order.
865+ - ` QueryAnalyzerTest.shouldResolveCastProjectionColumnFromItsAlias ` ,
866+ ` QueryAnalyzerTest.shouldResolveEnumCastProjectionColumn ` ,
867+ ` QueryAnalyzerTest.shouldResolveArrayCastProjectionColumn ` ,
868+ ` QueryAnalyzerTest.shouldRejectUnsupportedCastProjectionForm ` , and
869+ ` QueryAnalyzerTest.shouldRejectCastPlaceholderProjection ` cover both cast
870+ spellings and both alias spellings, the nullable column of the cast type
871+ including a declared enum and an array, the missing alias and the unmapped
872+ cast type, and the projected placeholder that stays rejected, and
873+ ` PostgresIntegrationTest.shouldExecuteGeneratedCastProjectionAgainstPostgres `
874+ executes a ` :one ` cast ` SUM ` against PostgreSQL 16 whose ` BigDecimal `
875+ component is the sum and ` null ` when no row matches.
819876- ` QueryAnalyzerTest.shouldAnalyzeLeftJoinWithNullableJoinedColumns ` ,
820877 ` QueryAnalyzerTest.shouldExpandAllColumnsOfLeftJoinedSourceAsNullable ` ,
821878 ` QueryAnalyzerTest.shouldExpandQualifiedAllColumnsOfLeftJoinedSourceAsNullable ` ,
0 commit comments