From 0e69748880c86813d44cb6ba07197646a570f55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20=C3=87engel?= Date: Mon, 12 Oct 2026 00:38:20 +0300 Subject: [PATCH] test(compiler): run reference service query shapes and rejections Add an eleventh reference migration with a task assignee, a soft-deleted user column, and a partial unique handle index, mirrored in the snapshot, and eleven queries that run through the generated repository against PostgreSQL 16: a dynamic sort and cast page, EXISTS, IN (SELECT ...), and scalar subqueries, composite-key and filtered left joins, untargeted, named-constraint, and predicated conflict clauses, DO UPDATE ... WHERE, and an aliased RETURNING. Add an unsupported query catalogue with one file per rejected shape, each asserted with its whole located diagnostic and no generated output, and list the new tests in the query and PostgreSQL documentation. --- docs/postgresql.md | 2 +- docs/queries.md | 22 + .../ReferenceServiceIntegrationTest.java | 473 +++++++++++++++++- .../V11__assignees_and_soft_deleted_users.sql | 7 + .../resources/reference-service/queries.sql | 90 ++++ .../resources/reference-service/schema.sql | 6 +- .../unsupported/queries/comma_join.sql | 6 + .../unsupported/queries/cross_join.sql | 6 + .../unsupported/queries/cte_source.sql | 7 + .../unsupported/queries/delete_using.sql | 7 + .../unsupported/queries/from_subquery.sql | 7 + .../unsupported/queries/full_join.sql | 6 + .../unsupported/queries/insert_select.sql | 5 + .../unsupported/queries/lateral_join.sql | 10 + .../unsupported/queries/multi_row_values.sql | 5 + .../unsupported/queries/natural_join.sql | 6 + .../unsupported/queries/right_join.sql | 6 + .../queries/subscripted_column.sql | 6 + .../unsupported/queries/uncast_lower.sql | 6 + .../unsupported/queries/union.sql | 7 + .../unsupported/queries/union_all.sql | 7 + .../unsupported/queries/update_from.sql | 7 + .../unsupported/queries/using_join.sql | 6 + 23 files changed, 707 insertions(+), 3 deletions(-) create mode 100644 sqlcj-cli/src/test/resources/reference-service/migrations/V11__assignees_and_soft_deleted_users.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/comma_join.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/cross_join.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/cte_source.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/delete_using.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/from_subquery.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/full_join.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/insert_select.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/lateral_join.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/multi_row_values.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/natural_join.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/right_join.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/subscripted_column.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/uncast_lower.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/union.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/union_all.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/update_from.sql create mode 100644 sqlcj-cli/src/test/resources/reference-service/unsupported/queries/using_join.sql diff --git a/docs/postgresql.md b/docs/postgresql.md index 9ad6929..24e5a6d 100644 --- a/docs/postgresql.md +++ b/docs/postgresql.md @@ -838,7 +838,7 @@ sqlcj: Invalid query 'ListTags' in /home/dev/project/queries.sql at line 5: Colu Schema snapshot input: - `ReferenceServiceIntegrationTest.shouldGenerateIdenticalRepositoriesFromHistorySnapshotAndDump` - applies the reference service's ten migrations to PostgreSQL 16 with `psql`, + applies the reference service's eleven migrations to PostgreSQL 16 with `psql`, runs `pg_dump --schema-only` against the migrated database, and generates one byte-identical, compiling output tree, manifest included, from the migration history, the hand-written snapshot, and that dump. diff --git a/docs/queries.md b/docs/queries.md index 7eb3bb3..f86a80f 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -1540,3 +1540,25 @@ Generated Java: `java.util.List` import, and `PostgresIntegrationTest.shouldRoundTripArrayValues` executes them against PostgreSQL 16. + +Reference service: + +- `ReferenceServiceIntegrationTest.shouldExecuteTheGeneratedMilestoneQueryShapes` + executes the reference service's dynamic sort in both of its modes and a cast + page with an offset, its `EXISTS` and `IN (SELECT ...)` membership checks and + its scalar subquery, its composite-key join and the filtered left join that + names only a live assignee, its untargeted `ON CONFLICT DO NOTHING`, its + `ON CONFLICT ON CONSTRAINT ... DO UPDATE ... WHERE` that skips an unchanged + role, its conflict-target predicate over a partial unique index, and the + aliased `RETURNING` of an optimistic update, against PostgreSQL 16 through the + generated repository. +- `ReferenceServiceIntegrationTest.shouldRejectEachUnsupportedCatalogueQuery` + covers the whole located diagnostic line, and the absence of generated + output, of every entry of the reference service's unsupported query + catalogue: the two set operations, the `RIGHT`, `FULL`, `USING`, `NATURAL`, + `CROSS`, comma, and `LATERAL` joins, a common table expression used as a + source, a `FROM` subquery, a multi-row `VALUES`, an `INSERT ... SELECT`, an + `UPDATE ... FROM`, a `DELETE ... USING`, an uncast `lower(:email)`, and a + placeholder beside a subscripted column, and + `ReferenceServiceIntegrationTest.shouldCatalogueExactlyTheUnsupportedQueryFiles` + covers that the catalogue directory holds exactly those queries. diff --git a/sqlcj-cli/src/test/java/dev/sqlcj/compiler/ReferenceServiceIntegrationTest.java b/sqlcj-cli/src/test/java/dev/sqlcj/compiler/ReferenceServiceIntegrationTest.java index dbce362..283d3e7 100644 --- a/sqlcj-cli/src/test/java/dev/sqlcj/compiler/ReferenceServiceIntegrationTest.java +++ b/sqlcj-cli/src/test/java/dev/sqlcj/compiler/ReferenceServiceIntegrationTest.java @@ -9,6 +9,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.postgresql.ds.PGSimpleDataSource; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.Container; @@ -86,7 +88,103 @@ class ReferenceServiceIntegrationTest { "V7__roles_and_row_level_security.sql", "V8__enum_and_column_changes.sql", "V9__status_index.sql", - "V10__seed_and_view_refresh.sql" + "V10__seed_and_view_refresh.sql", + "V11__assignees_and_soft_deleted_users.sql" + ); + + /** + * The unsupported query catalogue: one query file per shape the supported + * subset excludes, with the query name and the reason sqlcj reports for it. + */ + private static final List UNSUPPORTED_QUERIES = List.of( + new UnsupportedQuery( + "union.sql", + "ListProjectAndTaskTitles", + "UNION, INTERSECT, and EXCEPT are not supported; write one query per SELECT." + ), + new UnsupportedQuery( + "union_all.sql", + "ListEveryProjectAndTaskTitle", + "UNION, INTERSECT, and EXCEPT are not supported; write one query per SELECT." + ), + new UnsupportedQuery( + "right_join.sql", + "ListProjectsWithTasks", + "RIGHT JOIN is not supported; write a LEFT JOIN with the two sources swapped." + ), + new UnsupportedQuery( + "full_join.sql", + "ListProjectsAndTasks", + "FULL JOIN is not supported." + ), + new UnsupportedQuery( + "using_join.sql", + "ListTaskComments", + "JOIN ... USING is not supported; write the join condition with ON." + ), + new UnsupportedQuery( + "natural_join.sql", + "ListNaturallyJoinedTasks", + "NATURAL JOIN is not supported; write the join condition with ON." + ), + new UnsupportedQuery( + "cross_join.sql", + "ListOrganizationProjectPairs", + "CROSS JOIN is not supported; write JOIN ... ON TRUE." + ), + new UnsupportedQuery( + "comma_join.sql", + "ListCommaJoinedTasks", + "A comma-separated FROM list is not supported; " + + "write JOIN ... ON with the join condition." + ), + new UnsupportedQuery( + "lateral_join.sql", + "ListProjectsWithLatestTask", + "LATERAL is not supported." + ), + new UnsupportedQuery( + "cte_source.sql", + "ListOpenTaskTitles", + "A common table expression used as a source is not supported: open_tasks" + ), + new UnsupportedQuery( + "from_subquery.sql", + "ListRecentTaskTitles", + "A subquery in FROM is not supported." + ), + new UnsupportedQuery( + "multi_row_values.sql", + "CreateTwoProjects", + "INSERT requires a single VALUES row." + ), + new UnsupportedQuery( + "insert_select.sql", + "ArchiveAuditLog", + "INSERT ... SELECT is not supported." + ), + new UnsupportedQuery( + "update_from.sql", + "RaiseProjectTaskPriority", + "UPDATE ... FROM and joined updates are not supported." + ), + new UnsupportedQuery( + "delete_using.sql", + "DeleteProjectComments", + "DELETE ... USING and joined deletes are not supported." + ), + new UnsupportedQuery( + "uncast_lower.sql", + "FindUserByEmail", + "Cannot infer the type of :email in lower(:email); " + + "state it with a cast, such as :email::text" + ), + new UnsupportedQuery( + "subscripted_column.sql", + "ListTasksByFirstLabel", + "Cannot infer the type of :label in labels[1] = :label; " + + "state it with a cast, such as :label::text" + ) ); /** The files the three schema inputs generate, relative to {@code java.out}. */ @@ -402,6 +500,336 @@ void shouldExecuteTheGeneratedReferenceServiceQueries() throws Exception { } } + /** + * The generated repository executes the query shapes this milestone adds + * against the migrated database: the dynamic sort and the cast page, the + * {@code EXISTS}, {@code IN (SELECT ...)}, and scalar subqueries, the + * composite-key join, the filtered left join, the conflict clauses that + * bind nothing, and the aliased {@code RETURNING} of an optimistic update. + * + *

The database is shared with the other tests of this class, so this one + * works inside its own organization, project, and users, and writes no + * audit entry. + */ + @Test + void shouldExecuteTheGeneratedMilestoneQueryShapes() throws Exception { + Path generatedDirectory = tempDir.resolve("generated-shapes"); + + generate(List.of(migrations().toString()), generatedDirectory); + + Path classesDirectory = compile(generatedDirectory, tempDir.resolve("classes-shapes")); + + try (URLClassLoader classLoader = classLoader(classesDirectory)) { + Object repository = newRepository(classLoader); + + Long organizationId = (Long) component( + invoke(repository, "createOrganization", "Shape Test", "{}"), + "id" + ); + + Long assigneeId = (Long) component( + invoke(repository, "createUser", organizationId, "lin@example.com", "Lin Shu"), + "id" + ); + + Long retiredId = (Long) component( + invoke(repository, "createUser", organizationId, "kurt@example.com", "Kurt Godel"), + "id" + ); + + Object project = invoke(repository, "createProject", organizationId, "SHAPE", "Shapes"); + + Long projectId = (Long) component(project, "id"); + + Object open = enumConstant(classLoader, "TaskStatus", "OPEN"); + Object maintainer = enumConstant(classLoader, "MembershipRole", "MAINTAINER"); + + Long betaId = (Long) component( + createShapeTask(repository, projectId, "Beta", (short) 3, open), + "id" + ); + + Long alphaId = (Long) component( + createShapeTask(repository, projectId, "Alpha", (short) 1, open), + "id" + ); + + Long gammaId = (Long) component( + createShapeTask(repository, projectId, "Gamma", (short) 2, open), + "id" + ); + + assertEquals( + List.of("Alpha", "Beta", "Gamma"), + componentsOf( + (List) invoke(repository, "listTasksPage", projectId, "title", 3, 0), + "title" + ), + "the dynamic sort orders by title" + ); + + assertEquals( + List.of("Alpha", "Gamma", "Beta"), + componentsOf( + (List) invoke(repository, "listTasksPage", projectId, "priority", 3, 0), + "title" + ), + "the same query orders by priority" + ); + + assertEquals( + List.of("Beta", "Gamma"), + componentsOf( + (List) invoke(repository, "listTasksPage", projectId, "title", 2, 1), + "title" + ), + "the cast page skips its offset" + ); + + assertEquals( + 1, + invoke(repository, "joinOrganization", organizationId, assigneeId), + "the untargeted ON CONFLICT DO NOTHING inserts the first membership" + ); + + assertEquals( + 0, + invoke(repository, "joinOrganization", organizationId, assigneeId), + "the untargeted ON CONFLICT DO NOTHING keeps the existing membership" + ); + + assertEquals( + 1, + invoke(repository, "changeMemberRole", organizationId, assigneeId, maintainer), + "ON CONFLICT ON CONSTRAINT updates the changed role" + ); + + assertEquals( + 0, + invoke(repository, "changeMemberRole", organizationId, assigneeId, maintainer), + "DO UPDATE ... WHERE skips the unchanged role" + ); + + List visibleProjects = (List) invoke( + repository, + "listVisibleProjects", + assigneeId + ); + + assertEquals(1, visibleProjects.size(), "EXISTS keeps only the member's projects"); + + assertEquals(projectId, component(visibleProjects.get(0), "id")); + + assertEquals("SHAPE", component(visibleProjects.get(0), "code")); + + assertEquals( + List.of("Beta", "Alpha", "Gamma"), + componentsOf( + (List) invoke(repository, "listOrganizationTasks", organizationId), + "title" + ), + "IN (SELECT ...) keeps the tasks of the organization's projects" + ); + + assertNotNull(invoke(repository, "addComment", alphaId, assigneeId, "First.")); + + assertNotNull(invoke(repository, "addComment", alphaId, assigneeId, "Second.")); + + OffsetDateTime beforeTheComments = OffsetDateTime.of( + 2026, + 1, + 1, + 0, + 0, + 0, + 0, + ZoneOffset.UTC + ); + + List recentComments = (List) invoke( + repository, + "listTasksWithRecentComments", + beforeTheComments, + projectId + ); + + assertEquals( + List.of(betaId, alphaId, gammaId), + componentsOf(recentComments, "id") + ); + + assertEquals( + List.of(0L, 2L, 0L), + componentsOf(recentComments, "recentComments"), + "the scalar subquery counts each task's recent comments" + ); + + assertEquals( + List.of(0L, 0L, 0L), + componentsOf( + (List) invoke( + repository, + "listTasksWithRecentComments", + beforeTheComments.plusYears(100), + projectId + ), + "recentComments" + ), + "the scalar subquery binds its own cast placeholder" + ); + + Object assignment = ((Optional) invoke( + repository, + "assignTask", + assigneeId, + alphaId, + 1 + )).orElseThrow(); + + assertEquals( + alphaId, + component(assignment, "taskId"), + "the aliased RETURNING names its own components" + ); + + assertEquals(2, component(assignment, "newVersion")); + + assertEquals( + Optional.empty(), + invoke(repository, "assignTask", assigneeId, alphaId, 1), + "an optimistic update of a stale version returns no row" + ); + + assertEquals( + 2, + component( + ((Optional) invoke(repository, "assignTask", retiredId, betaId, 1)) + .orElseThrow(), + "newVersion" + ) + ); + + assertEquals( + 1, + invoke(repository, "softDeleteUser", retiredId), + "the soft delete marks the assignee deleted" + ); + + List assignedTasks = (List) invoke(repository, "listAssignedTasks", projectId); + + assertEquals( + 1, + assignedTasks.size(), + "the composite-key join keeps only tasks assigned to a member" + ); + + assertEquals(alphaId, component(assignedTasks.get(0), "id")); + + assertEquals( + maintainer, + component(assignedTasks.get(0), "role"), + "the composite-key join reads the membership role" + ); + + List withAssignee = (List) invoke(repository, "listTasksWithAssignee", projectId); + + assertEquals( + List.of(betaId, alphaId, gammaId), + componentsOf(withAssignee, "id") + ); + + assertEquals( + Arrays.asList(null, "Lin Shu", null), + componentsOf(withAssignee, "assigneeName"), + "the filtered left join names the live assignee and no other" + ); + + Object claimed = ((Optional) invoke( + repository, + "claimHandle", + organizationId, + "ida@example.com", + "ida", + "Ida Rhodes" + )).orElseThrow(); + + assertNotNull(component(claimed, "id")); + + assertEquals( + "Ida Rhodes", + component(claimed, "displayName"), + "the conflict-target predicate claims a free handle" + ); + + assertEquals( + Optional.empty(), + invoke( + repository, + "claimHandle", + organizationId, + "ida.rhodes@example.com", + "ida", + "Ida Rhodes" + ), + "the conflict-target predicate leaves a live handle to its owner" + ); + } + } + + /** + * Every entry of the unsupported query catalogue is rejected with its whole + * located diagnostic line against the migration history, and writes no + * file. + */ + @ParameterizedTest + @MethodSource("unsupportedQueries") + void shouldRejectEachUnsupportedCatalogueQuery(UnsupportedQuery entry) throws Exception { + Path queriesFile = resource("unsupported/queries/" + entry.fileName()); + + Path generatedDirectory = tempDir.resolve("generated-unsupported"); + + CompilationException exception = assertThrows( + CompilationException.class, + () -> generate( + List.of(migrations().toString()), + queriesFile, + generatedDirectory + ) + ); + + assertEquals( + "Invalid query '%s' in %s at line 2: %s".formatted( + entry.queryName(), + queriesFile, + entry.reason() + ), + exception.getMessage() + ); + + assertFalse(Files.exists(generatedDirectory)); + } + + /** The catalogue directory holds exactly the catalogued query files. */ + @Test + void shouldCatalogueExactlyTheUnsupportedQueryFiles() throws Exception { + try (Stream files = Files.list(resource("unsupported/queries"))) { + assertEquals( + UNSUPPORTED_QUERIES.stream() + .map(UnsupportedQuery::fileName) + .sorted() + .toList(), + files + .map(file -> file.getFileName().toString()) + .sorted() + .toList() + ); + } + } + + static List unsupportedQueries() { + return UNSUPPORTED_QUERIES; + } + /** * The unsupported catalogue's {@code SET DATA TYPE}, applied after the * history, is rejected with its file, line, and column, and nothing is @@ -659,6 +1087,37 @@ private Object component(Object record, String componentName) throws Exception { .invoke(record); } + /** One component of every row of a result list, in the list's order. */ + private List componentsOf(List rows, String componentName) throws Exception { + List values = new ArrayList<>(); + + for (Object row : rows) { + values.add(component(row, componentName)); + } + + return values; + } + + /** One task of the milestone shapes, with the labels and metadata it needs. */ + private Object createShapeTask( + Object repository, + Long projectId, + String title, + Short priority, + Object status + ) throws Exception { + return invoke( + repository, + "createTask", + projectId, + title, + status, + priority, + List.of("shape"), + "{\"source\": \"fixture\"}" + ); + } + private List recordComponentNames(Class recordClass) { assertTrue(recordClass.isRecord()); @@ -666,4 +1125,16 @@ private List recordComponentNames(Class recordClass) { .map(RecordComponent::getName) .toList(); } + + /** + * One entry of the unsupported query catalogue: the query file, the name of + * the single query it holds, and the reason sqlcj reports for it. + */ + record UnsupportedQuery(String fileName, String queryName, String reason) { + + @Override + public String toString() { + return fileName; + } + } } diff --git a/sqlcj-cli/src/test/resources/reference-service/migrations/V11__assignees_and_soft_deleted_users.sql b/sqlcj-cli/src/test/resources/reference-service/migrations/V11__assignees_and_soft_deleted_users.sql new file mode 100644 index 0000000..43dbff5 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/migrations/V11__assignees_and_soft_deleted_users.sql @@ -0,0 +1,7 @@ +-- The task assignee, the soft-deleted user, and the live handle index. + +ALTER TABLE users ADD COLUMN deleted_at timestamptz; + +ALTER TABLE tasks ADD COLUMN assignee_id bigint REFERENCES users (id) ON DELETE SET NULL; + +CREATE UNIQUE INDEX users_live_handle_key ON users (organization_id, handle) WHERE deleted_at IS NULL; diff --git a/sqlcj-cli/src/test/resources/reference-service/queries.sql b/sqlcj-cli/src/test/resources/reference-service/queries.sql index d4be31e..0dbea89 100644 --- a/sqlcj-cli/src/test/resources/reference-service/queries.sql +++ b/sqlcj-cli/src/test/resources/reference-service/queries.sql @@ -82,3 +82,93 @@ ORDER BY id; SELECT * FROM audit_log_archive ORDER BY id; + +-- name: ListTasksPage :many +SELECT * +FROM tasks +WHERE project_id = :projectId +ORDER BY CASE WHEN :sort::text = 'title' THEN title END, + CASE WHEN :sort::text = 'priority' THEN priority END, + id +LIMIT :pageSize::int OFFSET :pageOffset::int; + +-- name: ListVisibleProjects :many +SELECT p.id, + p.code, + p.name +FROM projects p +WHERE EXISTS (SELECT 1 + FROM memberships m + WHERE m.organization_id = p.organization_id + AND m.user_id = :userId::bigint) +ORDER BY p.id; + +-- name: ListOrganizationTasks :many +SELECT t.id, + t.title +FROM tasks t +WHERE t.project_id IN (SELECT p.id + FROM projects p + WHERE p.organization_id = :organizationId::bigint) +ORDER BY t.id; + +-- name: ListTasksWithRecentComments :many +SELECT t.id, + (SELECT COUNT(*) + FROM comments c + WHERE c.task_id = t.id + AND c.created_at >= :since::timestamptz)::bigint AS recent_comments +FROM tasks t +WHERE t.project_id = :projectId +ORDER BY t.id; + +-- name: ListAssignedTasks :many +SELECT t.id, + t.title, + m.role +FROM tasks t + JOIN projects p ON p.id = t.project_id + JOIN memberships m ON m.organization_id = p.organization_id AND m.user_id = t.assignee_id +WHERE t.project_id = :projectId +ORDER BY t.id; + +-- name: ListTasksWithAssignee :many +SELECT t.id, + t.title, + u.display_name AS assignee_name +FROM tasks t + LEFT JOIN users u ON u.id = t.assignee_id AND u.deleted_at IS NULL +WHERE t.project_id = :projectId +ORDER BY t.id; + +-- name: JoinOrganization :exec +INSERT INTO memberships (organization_id, user_id) +VALUES (:organizationId, :userId) +ON CONFLICT DO NOTHING; + +-- name: ChangeMemberRole :exec +INSERT INTO memberships (organization_id, user_id, role) +VALUES (:organizationId, :userId, :role) +ON CONFLICT ON CONSTRAINT memberships_pkey DO UPDATE + SET role = EXCLUDED.role +WHERE memberships.role <> EXCLUDED.role; + +-- name: ClaimHandle :optional +INSERT INTO users (organization_id, email, handle, display_name) +VALUES (:organizationId, :email::text, :handle::text, :displayName) +ON CONFLICT (organization_id, handle) WHERE deleted_at IS NULL DO NOTHING +RETURNING id, display_name; + +-- name: AssignTask :optional +UPDATE tasks +SET assignee_id = :assigneeId, + version = version + 1 +WHERE id = :id + AND version = :version +RETURNING id AS task_id, version AS new_version; + +-- name: SoftDeleteUser :exec +UPDATE users +SET deleted_at = now() +WHERE id = :id + AND deleted_at IS NULL; diff --git a/sqlcj-cli/src/test/resources/reference-service/schema.sql b/sqlcj-cli/src/test/resources/reference-service/schema.sql index 0273a7e..ef65675 100644 --- a/sqlcj-cli/src/test/resources/reference-service/schema.sql +++ b/sqlcj-cli/src/test/resources/reference-service/schema.sql @@ -39,9 +39,12 @@ CREATE TABLE users handle citext, display_name text NOT NULL, last_login_ip inet, - created_at timestamptz NOT NULL DEFAULT now() + created_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz ); +CREATE UNIQUE INDEX users_live_handle_key ON users (organization_id, handle) WHERE deleted_at IS NULL; + CREATE TABLE projects ( id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, @@ -73,6 +76,7 @@ CREATE TABLE tasks estimate_hours numeric, title_length integer GENERATED ALWAYS AS (length(title)) STORED, created_at timestamptz NOT NULL DEFAULT now(), + assignee_id bigint REFERENCES users (id) ON DELETE SET NULL, CHECK (priority BETWEEN 1 AND 5) ); diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/comma_join.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/comma_join.sql new file mode 100644 index 0000000..47cf66f --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/comma_join.sql @@ -0,0 +1,6 @@ +-- Outside the supported set: write JOIN ... ON with the join condition. +-- name: ListCommaJoinedTasks :many +SELECT t.title, + p.name +FROM tasks t, projects p +WHERE p.id = t.project_id; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/cross_join.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/cross_join.sql new file mode 100644 index 0000000..6e83ff4 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/cross_join.sql @@ -0,0 +1,6 @@ +-- Outside the supported set: write JOIN ... ON TRUE. +-- name: ListOrganizationProjectPairs :many +SELECT o.name AS organization_name, + p.name AS project_name +FROM organizations o + CROSS JOIN projects p; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/cte_source.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/cte_source.sql new file mode 100644 index 0000000..f85a6da --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/cte_source.sql @@ -0,0 +1,7 @@ +-- Outside the supported set: a common table expression is not a source. +-- name: ListOpenTaskTitles :many +WITH open_tasks AS (SELECT id, title + FROM tasks + WHERE status = 'open') +SELECT title +FROM open_tasks; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/delete_using.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/delete_using.sql new file mode 100644 index 0000000..f9d7a6b --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/delete_using.sql @@ -0,0 +1,7 @@ +-- Outside the supported set: sqlcj models no DELETE ... USING. +-- name: DeleteProjectComments :exec +DELETE +FROM comments c + USING tasks t +WHERE t.id = c.task_id + AND t.project_id = :projectId; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/from_subquery.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/from_subquery.sql new file mode 100644 index 0000000..8288821 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/from_subquery.sql @@ -0,0 +1,7 @@ +-- Outside the supported set: sqlcj models no subquery source. +-- name: ListRecentTaskTitles :many +SELECT recent.title +FROM (SELECT t.title + FROM tasks t + ORDER BY t.id DESC + LIMIT 10) recent; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/full_join.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/full_join.sql new file mode 100644 index 0000000..72e19ce --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/full_join.sql @@ -0,0 +1,6 @@ +-- Outside the supported set: sqlcj models no FULL JOIN. +-- name: ListProjectsAndTasks :many +SELECT p.name, + t.title +FROM projects p + FULL JOIN tasks t ON t.project_id = p.id; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/insert_select.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/insert_select.sql new file mode 100644 index 0000000..9eae390 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/insert_select.sql @@ -0,0 +1,5 @@ +-- Outside the supported set: sqlcj models no INSERT ... SELECT. +-- name: ArchiveAuditLog :exec +INSERT INTO audit_log_archive (occurred_at, actor_id, action, detail) +SELECT occurred_at, actor_id, action, detail +FROM audit_log; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/lateral_join.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/lateral_join.sql new file mode 100644 index 0000000..d6a9c74 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/lateral_join.sql @@ -0,0 +1,10 @@ +-- Outside the supported set: sqlcj models no LATERAL source. +-- name: ListProjectsWithLatestTask :many +SELECT p.name, + latest.title +FROM projects p + JOIN LATERAL (SELECT t.title + FROM tasks t + WHERE t.project_id = p.id + ORDER BY t.id DESC + LIMIT 1) latest ON TRUE; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/multi_row_values.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/multi_row_values.sql new file mode 100644 index 0000000..b2a81a7 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/multi_row_values.sql @@ -0,0 +1,5 @@ +-- Outside the supported set: INSERT writes a single VALUES row. +-- name: CreateTwoProjects :exec +INSERT INTO projects (organization_id, code, name) +VALUES (:organizationId, :firstCode, :firstName), + (:organizationId, :secondCode, :secondName); diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/natural_join.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/natural_join.sql new file mode 100644 index 0000000..3cb026f --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/natural_join.sql @@ -0,0 +1,6 @@ +-- Outside the supported set: write the join condition with ON. +-- name: ListNaturallyJoinedTasks :many +SELECT t.title, + p.name +FROM tasks t + NATURAL JOIN projects p; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/right_join.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/right_join.sql new file mode 100644 index 0000000..428ca33 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/right_join.sql @@ -0,0 +1,6 @@ +-- Outside the supported set: write a LEFT JOIN with the sources swapped. +-- name: ListProjectsWithTasks :many +SELECT p.name, + t.title +FROM tasks t + RIGHT JOIN projects p ON p.id = t.project_id; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/subscripted_column.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/subscripted_column.sql new file mode 100644 index 0000000..e225c41 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/subscripted_column.sql @@ -0,0 +1,6 @@ +-- Outside the supported set: the placeholder states no type. +-- name: ListTasksByFirstLabel :many +SELECT id, + title +FROM tasks +WHERE labels[1] = :label; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/uncast_lower.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/uncast_lower.sql new file mode 100644 index 0000000..9a136ab --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/uncast_lower.sql @@ -0,0 +1,6 @@ +-- Outside the supported set: the placeholder states no type. +-- name: FindUserByEmail :optional +SELECT id, + display_name +FROM users +WHERE lower(email::text) = lower(:email); diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/union.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/union.sql new file mode 100644 index 0000000..0673266 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/union.sql @@ -0,0 +1,7 @@ +-- Outside the supported set: write one query per SELECT. +-- name: ListProjectAndTaskTitles :many +SELECT name AS title +FROM projects +UNION +SELECT title +FROM tasks; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/union_all.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/union_all.sql new file mode 100644 index 0000000..3190c4d --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/union_all.sql @@ -0,0 +1,7 @@ +-- Outside the supported set: write one query per SELECT. +-- name: ListEveryProjectAndTaskTitle :many +SELECT name AS title +FROM projects +UNION ALL +SELECT title +FROM tasks; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/update_from.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/update_from.sql new file mode 100644 index 0000000..aa13db3 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/update_from.sql @@ -0,0 +1,7 @@ +-- Outside the supported set: sqlcj models no UPDATE ... FROM. +-- name: RaiseProjectTaskPriority :exec +UPDATE tasks t +SET priority = 1 +FROM projects p +WHERE p.id = t.project_id + AND p.code = :code; diff --git a/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/using_join.sql b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/using_join.sql new file mode 100644 index 0000000..8e70772 --- /dev/null +++ b/sqlcj-cli/src/test/resources/reference-service/unsupported/queries/using_join.sql @@ -0,0 +1,6 @@ +-- Outside the supported set: write the join condition with ON. +-- name: ListTaskComments :many +SELECT t.title, + c.body +FROM tasks t + JOIN comments c USING (id);