Skip to content

Commit 8919f88

Browse files
authored
Merge pull request #76 from sqlcj-dev/feature/unsupported-query-diagnostics-and-internal-errors
feat: state unsupported queries in SQL terms and flag internal errors
2 parents 6882912 + 6815cf9 commit 8919f88

5 files changed

Lines changed: 1145 additions & 93 deletions

File tree

‎docs/queries.md‎

Lines changed: 120 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,14 @@ configuration entry.
7777

7878
### Sources
7979

80-
- The `FROM` item must be a table of that schema, optionally with an alias.
80+
- The `FROM` item must be a table of that schema, optionally with an alias. A
81+
`LATERAL` source is rejected as `LATERAL is not supported.` and a subquery
82+
source as `A subquery in FROM is not supported.`
83+
- A `FROM` or `JOIN` source that names a `WITH` clause of its own query, which
84+
in PostgreSQL reads that query and not the table of the same name, is rejected
85+
as `A common table expression used as a source is not supported: <name>`
86+
before the schema is consulted, so a `WITH` clause named like a table never
87+
reads that table.
8188
- sqlcj models one namespace, so a source resolves by its unqualified name:
8289
`users`, `public.users`, and `"public"."users"` are one table, matched without
8390
SQL identifier delimiters and case-insensitively. A source qualified with any
@@ -87,10 +94,21 @@ configuration entry.
8794
not followed; see
8895
[Schema Snapshot Input](postgresql.md#schema-snapshot-input).
8996
- A table may be joined with `JOIN`, `INNER JOIN`, `LEFT JOIN`, or
90-
`LEFT OUTER JOIN`, and inner and left joins may be chained in any order. A
91-
comma-separated source list and every other join modifier — `RIGHT`, `FULL`, a
92-
bare `OUTER`, `CROSS`, `NATURAL`, `SEMI`, `APPLY`, `STRAIGHT`, `GLOBAL`, a
93-
join hint, and `USING (...)` — are rejected.
97+
`LEFT OUTER JOIN`, and inner and left joins may be chained in any order.
98+
- Every other join is rejected, and a join that can be written as a supported
99+
one says what to write:
100+
- `FULL JOIN is not supported.`
101+
- `RIGHT JOIN is not supported; write a LEFT JOIN with the two sources swapped.`
102+
- `CROSS JOIN is not supported; write JOIN ... ON TRUE.`
103+
- `NATURAL JOIN is not supported; write the join condition with ON.`
104+
- `JOIN ... USING is not supported; write the join condition with ON.`
105+
- `A comma-separated FROM list is not supported; write JOIN ... ON with the join condition.`
106+
107+
A `LATERAL` or subquery join source is reported as the same unsupported
108+
source as a `FROM` item of that kind, before the join kind, and every
109+
remaining modifier — a bare `OUTER`, `SEMI`, `APPLY`, `STRAIGHT`, `GLOBAL`,
110+
and a join hint — is rejected as
111+
`Only unmodified INNER JOIN and LEFT JOIN clauses are supported.`
94112
- Each join requires exactly one `ON` condition, which may be any boolean
95113
expression, including `ON TRUE`. Its column references, qualified or not,
96114
resolve against the sources introduced up to and including the joined source,
@@ -119,7 +137,10 @@ configuration entry.
119137
unknown column is rejected.
120138
- A result expression that is not a direct column, a wildcard, the scalar count
121139
below, or the cast projection below — such as another function call, an
122-
arithmetic expression, or a literal — is rejected.
140+
arithmetic expression, or a literal — is rejected as
141+
`Cannot infer the type of <expression> in SELECT; state it with a cast and an
142+
alias, such as SUM(amount)::numeric AS total`, naming the expression as the
143+
query writes it, on one line.
123144
- An explicit alias names the result column, so `SELECT id AS author_id`
124145
generates the record component `authorId` while the component's type still
125146
comes from the column and its nullability from the column and its source.
@@ -538,12 +559,18 @@ value is read back with its declared type. A clause that is exactly `*` returns
538559
the target table's shared `<TableName>Row` record, while a `RETURNING` column
539560
list keeps the query's own `<QueryName>Result` record.
540561

541-
An aliased `*`, a computed `RETURNING` item with or without an alias, a
542-
qualified `table.*`, an unknown column, and `RETURNING` on `:exec` are rejected.
543-
Multi-row `VALUES`, `INSERT ... SELECT`, `UPDATE ... FROM`, `DELETE ... USING`,
544-
and common table expressions are rejected in a returning write, while an
545-
accepted `ON CONFLICT` clause is analyzed the same way with and without
546-
`RETURNING`.
562+
An aliased `*`, a qualified `table.*`, an unknown column, and `RETURNING` on
563+
`:exec` are rejected, and a computed `RETURNING` item, with or without an alias,
564+
is rejected as
565+
`Unsupported RETURNING item <expression>; return columns of the target table or *`,
566+
naming the expression as the query writes it, on one line. A cast does not make
567+
such an item supported, because a `RETURNING` clause returns columns of the
568+
target table.
569+
570+
Multi-row `VALUES`, `INSERT ... SELECT`, `UPDATE ... FROM`, and
571+
`DELETE ... USING` are rejected in every write, and common table expressions in
572+
a returning write, while an accepted `ON CONFLICT` clause is analyzed the same
573+
way with and without `RETURNING`.
547574

548575
## Parameters
549576

@@ -869,20 +896,34 @@ the documented shapes are contract, tested, and safe to rely on.
869896

870897
### Rejected
871898

872-
Rejection stops the run with a diagnostic naming the query source, the query
873-
name, and its header line.
874-
899+
Rejection stops the run with one diagnostic line naming the query source, the
900+
query name, and its header line.
901+
902+
- A statement that is not a `SELECT`, an `INSERT`, an `UPDATE`, or a `DELETE`,
903+
including `VALUES`, `TABLE`, `MERGE`, `TRUNCATE`, `EXPLAIN`, `EXECUTE`, a DDL
904+
statement such as `CREATE TABLE`, and a `SET` statement, each rejected as
905+
`Only SELECT, INSERT, UPDATE, and DELETE statements are supported.` before the
906+
annotation is checked. A parenthesized `SELECT` is the query it encloses, at
907+
any depth, so it is analyzed as that query.
908+
- A set operation, `UNION`, `UNION ALL`, `INTERSECT`, or `EXCEPT`, written
909+
directly or inside parentheses, rejected as
910+
`UNION, INTERSECT, and EXCEPT are not supported; write one query per SELECT.`
875911
- A result expression that is not a direct column, a wildcard, an aliased
876912
`COUNT(*)`, or an aliased cast, including another function call, another
877913
aggregate, and a literal. A `COUNT(*)` without an alias, a cast projection
878914
without an alias, and a cast projection whose type sqlcj does not map each
879915
fail with their own diagnostic, while `COUNT(column)`,
880916
`COUNT(DISTINCT column)`, `COUNT(t.*)`, `pg_catalog.count(*)`,
881-
`lower(name) AS n`, and the `FILTER` and `OVER` forms keep the
882-
unsupported-expression rejection.
883-
- A `FROM` item that is not a table, a comma-separated source list, a join that
884-
is not a plain inner or left join, a join that does not carry exactly one
885-
`ON` condition, and a set operation such as `UNION`.
917+
`lower(name) AS n`, `name || 'x' AS n`, and the `FILTER` and `OVER` forms are
918+
rejected as `Cannot infer the type of COUNT(*) OVER () in SELECT; state it
919+
with a cast and an alias, such as SUM(amount)::numeric AS total`.
920+
- A `FROM` or `JOIN` source that is not a table of the schema: a `LATERAL`
921+
source, a subquery source, and a source that names a `WITH` clause of its own
922+
query, each with the diagnostic [Sources](#sources) states.
923+
- A join kind outside a plain inner or left join, each with the diagnostic
924+
[Sources](#sources) states, which says what to write instead for `RIGHT`,
925+
`CROSS`, `NATURAL`, `USING`, and a comma-separated `FROM` list, and a join
926+
that does not carry exactly one `ON` condition.
886927
- An uncast placeholder in a location sqlcj does not analyze, including
887928
`ORDER BY $1`, a projection such as `SELECT $1 AS x`, a grouping list, a
888929
`HAVING` predicate, a subquery, a
@@ -925,10 +966,9 @@ name, and its header line.
925966
placeholder, each of them also as the operand of a cast, such as
926967
`:"x"::text`, `&x::text`, or `:a.b::text`, one name whose occurrences have
927968
conflicting types, and non-contiguous or non-positive placeholder indexes.
928-
- An `INSERT` without an explicit column list, with more than one `VALUES` row,
929-
or built from a `SELECT`; an `UPDATE` assignment that sets a column list, such
930-
as `SET (name, active) = ('a', TRUE)`; and a written column that the table does
931-
not declare.
969+
- An `INSERT` without an explicit column list; an `UPDATE` assignment that sets
970+
a column list, such as `SET (name, active) = ('a', TRUE)`; and a written
971+
column that the table does not declare.
932972
- A conflict clause outside the accepted upsert shape, on an `:exec` and on a
933973
returning insert alike: a `DO UPDATE` without a conflict target, which is
934974
rejected naming the two targets it accepts, and a target element that is an
@@ -938,11 +978,27 @@ name, and its header line.
938978
unqualified conflict-predicate column that both the inserted table and
939979
`EXCLUDED` carry, or a predicate column qualified with a name its own
940980
predicate does not read, such as `EXCLUDED` in a conflict-target predicate.
941-
- An aliased `*`, a computed, qualified-wildcard, or unknown `RETURNING` item;
942-
`RETURNING` on `:exec`; and `UPDATE ... FROM`, `DELETE ... USING`, or a common
943-
table expression in a returning write.
981+
- An aliased `*`, a computed, qualified-wildcard, or unknown `RETURNING` item,
982+
and `RETURNING` on `:exec`; a computed item is rejected as
983+
[`RETURNING`](#returning) states.
984+
- The shapes that read or write more than the one analyzed table, on an
985+
`:exec` and on a returning write alike: an `INSERT` with more than one
986+
`VALUES` row, rejected as `INSERT requires a single VALUES row.`; an
987+
`INSERT ... SELECT`, rejected as `INSERT ... SELECT is not supported.`; an
988+
`UPDATE ... FROM` or joined `UPDATE`, rejected as
989+
`UPDATE ... FROM and joined updates are not supported.`; and a
990+
`DELETE ... USING` or joined `DELETE`, rejected as
991+
`DELETE ... USING and joined deletes are not supported.` A common table
992+
expression in a returning write is rejected too.
944993
- Any annotation other than `:one`, `:optional`, `:many`, and `:exec`.
945994

995+
An unexpected failure of schema parsing, query analysis, or generation is not a
996+
diagnostic of the compiled input: it is reported as one line that names the
997+
step's own input and ends `; please report this sqlcj defect`, such as
998+
`Internal error while analyzing query 'GetUser' in queries.sql at line 1;
999+
please report this sqlcj defect`. The line carries neither the message nor the
1000+
class of the failure, because neither describes the compiled input.
1001+
9461002
### Not analyzed
9471003

9481004
These are outside the subset and are not part of the contract. sqlcj neither
@@ -956,8 +1012,8 @@ them:
9561012
`LIKE` with a literal pattern, a range whose bounds are both literal such as
9571013
`id BETWEEN 1 AND 10`, `IN` with a subquery, or `= ANY` over something other
9581014
than a placeholder, such as `id = ANY('{1,2}')`,
959-
- common table expressions, and subqueries outside the `FROM` item,
960-
- `UPDATE ... FROM` and `DELETE ... USING` on a non-returning `:exec` write,
1015+
- a common table expression whose name no source reads, and subqueries outside
1016+
the `FROM` item,
9611017
- the unique index an accepted `ON CONFLICT` target matches, the constraint an
9621018
`ON CONSTRAINT` target names, the column references of an `EXCLUDED` value
9631019
that is not exactly `EXCLUDED.column`, and a conflict-predicate column that
@@ -1274,6 +1330,41 @@ Writes and `RETURNING`:
12741330
insert, a returning named insert, and an `:exec` update against
12751331
PostgreSQL 16.
12761332

1333+
Unsupported queries and internal errors:
1334+
1335+
- `SqlcjCompilerIntegrationTest.shouldRejectUnsupportedQueryBeforeWriting`
1336+
covers the whole located diagnostic line, and the absence of generated
1337+
output, of each rejected input: the set operations written directly and in
1338+
parentheses, the `VALUES`, `TABLE`, `MERGE`, `TRUNCATE`, `EXPLAIN`,
1339+
`EXECUTE`, `CREATE TABLE`, and `SET` statements, a window and a
1340+
concatenation projection, a computed `RETURNING` item, the `RIGHT`, `FULL`,
1341+
`USING`, `NATURAL`, `CROSS`, comma, and `LATERAL` joins, a `FROM` subquery, a
1342+
common table expression used as a source and one named like a schema table,
1343+
a multi-row `VALUES`, an `INSERT ... SELECT` on `:exec` and with
1344+
`RETURNING`, and an `:exec` `UPDATE ... FROM`, comma-joined `UPDATE`,
1345+
`DELETE ... USING`, and joined `DELETE`, while
1346+
`QueryAnalyzerTest.shouldRejectUnsupportedCountProjectionForm`,
1347+
`QueryAnalyzerTest.shouldRejectUnsupportedJoinModifier`,
1348+
`QueryAnalyzerTest.shouldRejectUnsupportedReturningItem`,
1349+
`QueryAnalyzerTest.shouldRejectExcludedReturningWriteForm`, and
1350+
`QueryAnalyzerTest.shouldRejectParenthesizedInsertQuerySource` cover the
1351+
projection, join, and write rejections per form, including the modifiers that
1352+
keep the general join rejection, and
1353+
`QueryAnalyzerTest.shouldAnalyzeParenthesizedSelectAsTheQueryItEncloses` and
1354+
`QueryAnalyzerTest.shouldAnalyzeInsertWhoseValuesRowIsParenthesized` cover
1355+
the doubly parenthesized read that analyzes like the query it encloses and
1356+
the parenthesized `VALUES` row that analyzes like the row it writes.
1357+
- `SqlcjCompilerIntegrationTest.shouldReportAnUnexpectedSchemaFailureAsAnInternalError`,
1358+
`SqlcjCompilerIntegrationTest.shouldReportAnUnexpectedAnalysisFailureAsAnInternalError`,
1359+
`SqlcjCompilerIntegrationTest.shouldReportAnUnexpectedGenerationFailureAsAnInternalError`,
1360+
and
1361+
`SqlcjCompilerIntegrationTest.shouldReportAnUnexpectedSharedGenerationFailureAsAnInternalError`
1362+
inject an unexpected failure into the schema, analysis, repository
1363+
generation, and shared row-record steps and cover the exact internal-error
1364+
line of each, that it carries neither the failure's message nor its class,
1365+
including a blank-message unsupported-operation failure and a
1366+
`NumberFormatException`, and that no file is written.
1367+
12771368
Parameters:
12781369

12791370
- `QueryAnalyzerTest.shouldResolveBindingIndexesInTextualOrder`,

0 commit comments

Comments
 (0)