Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -1956,11 +1956,19 @@ Reference service:
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.shouldExecuteTheGeneratedNestedQueriesAndMultiRowWrites`
executes the reference service's `NOT EXISTS` check, its CTE report joined to
`projects`, its aliased subquery source with a `LIMIT` placeholder, its
two-row insert sharing one placeholder, its
`INSERT ... SELECT ... unnest(:userIds::bigint[]) ON CONFLICT DO NOTHING`,
its `INSERT ... SELECT` from a table, its `UPDATE ... FROM` returning the
shared task row, and its `DELETE ... USING`, 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, an uncast
`lower(:email)`, and a placeholder beside a subscripted column, and
`CROSS`, comma, and `LATERAL` joins, a recursive and a data-modifying CTE, an
uncast `lower(:email)`, and a placeholder beside a subscripted column, and
`ReferenceServiceIntegrationTest.shouldCatalogueExactlyTheUnsupportedQueryFiles`
covers that the catalogue directory holds exactly those queries.
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ class ReferenceServiceIntegrationTest {
"ListProjectsWithLatestTask",
"LATERAL is not supported."
),
new UnsupportedQuery(
"recursive_cte.sql",
"ListPriorityLevels",
"WITH RECURSIVE is not supported."
),
new UnsupportedQuery(
"data_modifying_cte.sql",
"DeleteDoneTasks",
"A data-modifying statement in WITH is not supported; "
+ "run it as a query of its own."
),
new UnsupportedQuery(
"uncast_lower.sql",
"FindUserByEmail",
Expand Down Expand Up @@ -746,6 +757,213 @@ void shouldExecuteTheGeneratedMilestoneQueryShapes() throws Exception {
}
}

/**
* The generated repository executes the nested-query and multi-row write
* shapes this milestone adds against the migrated database: the
* {@code NOT EXISTS} check, the CTE report, the aliased subquery source
* with a {@code LIMIT} placeholder, the multi-row insert, the
* {@code unnest} bulk insert, the {@code INSERT ... SELECT} from a table,
* the joined update returning the shared task row, and the {@code USING}
* delete.
*
* <p>The database is shared with the other tests of this class, so this one
* works inside its own organization, projects, and users, and writes no
* audit entry.
*/
@Test
void shouldExecuteTheGeneratedNestedQueriesAndMultiRowWrites() throws Exception {
Path generatedDirectory = tempDir.resolve("generated-nested");

generate(List.of(migrations().toString()), generatedDirectory);

Path classesDirectory = compile(generatedDirectory, tempDir.resolve("classes-nested"));

try (URLClassLoader classLoader = classLoader(classesDirectory)) {
Object repository = newRepository(classLoader);

Long organizationId = (Long) component(
invoke(repository, "createOrganization", "Nest Test", "{}"),
"id"
);

Long authorId = (Long) component(
invoke(repository, "createUser", organizationId, "edsger@example.com", "Edsger Dijkstra"),
"id"
);

Long otherAuthorId = (Long) component(
invoke(repository, "createUser", organizationId, "barbara@example.com", "Barbara Liskov"),
"id"
);

Long nestedProjectId = (Long) component(
invoke(repository, "createProject", organizationId, "NEST", "Nested"),
"id"
);

Long copyProjectId = (Long) component(
invoke(repository, "createProject", organizationId, "COPY", "Copies"),
"id"
);

assertEquals(
2,
invoke(repository, "createTwoTasks", nestedProjectId, "Alpha", "Beta"),
"the multi-row insert writes both rows of its VALUES list"
);

assertEquals(
2,
invoke(repository, "addMembers", organizationId, List.of(authorId, otherAuthorId)),
"the unnest bulk insert writes one membership per array element"
);

assertEquals(
0,
invoke(repository, "addMembers", organizationId, List.of(authorId, otherAuthorId)),
"the unnest bulk insert keeps the existing memberships"
);

List<?> uncommented = (List<?>) invoke(
repository,
"listTasksUncommentedBy",
nestedProjectId,
authorId
);

assertEquals(
List.of("Alpha", "Beta"),
componentsOf(uncommented, "title"),
"both inserted tasks are uncommented"
);

Long alphaId = (Long) component(uncommented.get(0), "id");

assertNotNull(invoke(repository, "addComment", alphaId, authorId, "Mine."));

assertEquals(
List.of("Beta"),
componentsOf(
(List<?>) invoke(
repository,
"listTasksUncommentedBy",
nestedProjectId,
authorId
),
"title"
),
"NOT EXISTS drops the task its correlated author commented on"
);

assertEquals(
List.of("Alpha", "Beta"),
componentsOf(
(List<?>) invoke(
repository,
"listTasksUncommentedBy",
nestedProjectId,
otherAuthorId
),
"title"
),
"NOT EXISTS keeps every task the other author left uncommented"
);

List<?> onePriority = (List<?>) invoke(
repository,
"countOpenTasksByProject",
(short) 1,
organizationId
);

assertEquals(List.of("NEST"), componentsOf(onePriority, "code"));

assertEquals(
List.of(1L),
componentsOf(onePriority, "openTasks"),
"the CTE report counts the tasks its placeholder admits"
);

List<?> everyPriority = (List<?>) invoke(
repository,
"countOpenTasksByProject",
(short) 5,
organizationId
);

assertEquals(List.of("NEST"), componentsOf(everyPriority, "code"));

assertEquals(List.of(2L), componentsOf(everyPriority, "openTasks"));

assertEquals(
List.of("Beta"),
componentsOf(
(List<?>) invoke(repository, "listRecentTaskTitles", nestedProjectId, 1),
"title"
),
"the subquery source applies its own LIMIT placeholder"
);

assertEquals(
2,
invoke(repository, "copyOpenTasks", copyProjectId, nestedProjectId),
"INSERT ... SELECT copies the source project's open tasks"
);

List<?> raised = (List<?>) invoke(
repository,
"raiseProjectTaskPriority",
(short) 5,
organizationId,
"COPY"
);

assertEquals(2, raised.size(), "the joined update raises the copied tasks");

assertEquals(
"TasksRow",
raised.get(0).getClass().getSimpleName(),
"RETURNING t.* returns the shared row record"
);

assertEquals(
List.of("Alpha", "Beta"),
componentsOf(raised, "title").stream()
.map(String.class::cast)
.sorted()
.toList()
);

assertEquals(
List.of(copyProjectId, copyProjectId),
componentsOf(raised, "projectId")
);

assertEquals(
List.of((short) 5, (short) 5),
componentsOf(raised, "priority"),
"the joined update sets the priority its placeholder states"
);

assertEquals(
1,
invoke(repository, "deleteProjectComments", nestedProjectId),
"the USING delete removes the comments of its project's tasks"
);

List<?> bothProjects = (List<?>) invoke(
repository,
"countOpenTasksByProject",
(short) 5,
organizationId
);

assertEquals(List.of("COPY", "NEST"), componentsOf(bothProjects, "code"));

assertEquals(List.of(2L, 2L), componentsOf(bothProjects, "openTasks"));
}
}

/**
* Every entry of the unsupported query catalogue is rejected with its whole
* located diagnostic line against the migration history, and writes no
Expand Down
75 changes: 73 additions & 2 deletions sqlcj-cli/src/test/resources/reference-service/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ FROM projects p
WHERE EXISTS (SELECT 1
FROM memberships m
WHERE m.organization_id = p.organization_id
AND m.user_id = :userId::bigint)
AND m.user_id = :userId)
ORDER BY p.id;

-- name: ListOrganizationTasks :many
Expand All @@ -109,7 +109,7 @@ SELECT t.id,
FROM tasks t
WHERE t.project_id IN (SELECT p.id
FROM projects p
WHERE p.organization_id = :organizationId::bigint)
WHERE p.organization_id = :organizationId)
ORDER BY t.id;

-- name: ListTasksWithRecentComments :many
Expand Down Expand Up @@ -172,3 +172,74 @@ UPDATE users
SET deleted_at = now()
WHERE id = :id
AND deleted_at IS NULL;

-- name: ListTasksUncommentedBy :many
SELECT t.id,
t.title
FROM tasks t
WHERE t.project_id = :projectId
AND NOT EXISTS (SELECT 1
FROM comments c
WHERE c.task_id = t.id
AND c.author_id = :authorId)
ORDER BY t.id;

-- name: CountOpenTasksByProject :many
WITH open_tasks AS (SELECT project_id
FROM tasks
WHERE status = 'open'
AND priority <= :maxPriority)
SELECT p.code,
COUNT(*) AS open_tasks
FROM projects p
JOIN open_tasks o ON o.project_id = p.id
WHERE p.organization_id = :organizationId
GROUP BY p.code
ORDER BY p.code;

-- name: ListRecentTaskTitles :many
SELECT recent.id,
recent.title
FROM (SELECT t.id,
t.title
FROM tasks t
WHERE t.project_id = :projectId
ORDER BY t.id DESC
LIMIT :limit) recent
ORDER BY recent.title;

-- name: CreateTwoTasks :exec
INSERT INTO tasks (project_id, title, priority)
VALUES (:projectId, :firstTitle, 1),
(:projectId, :secondTitle, 2);

-- name: AddMembers :exec
INSERT INTO memberships (organization_id, user_id)
SELECT :organizationId, unnest(:userIds::bigint[])
ON CONFLICT DO NOTHING;

-- name: CopyOpenTasks :exec
INSERT INTO tasks (project_id, title, priority, labels)
SELECT :targetProjectId,
title,
priority,
labels
FROM tasks
WHERE project_id = :sourceProjectId
AND status = 'open';

-- name: RaiseProjectTaskPriority :many
UPDATE tasks t
SET priority = :priority
FROM projects p
WHERE p.id = t.project_id
AND p.organization_id = :organizationId
AND p.code = :code
RETURNING t.*;

-- name: DeleteProjectComments :exec
DELETE
FROM comments c
USING tasks t
WHERE t.id = c.task_id
AND t.project_id = :projectId;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Outside the supported set: a WITH body writes no rows.
-- name: DeleteDoneTasks :many
WITH removed AS (DELETE FROM tasks
WHERE status = 'done'
RETURNING id)
SELECT id
FROM removed;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Outside the supported set: sqlcj analyzes no recursive CTE.
-- name: ListPriorityLevels :many
WITH RECURSIVE levels AS (SELECT 1 AS level
UNION ALL
SELECT level + 1
FROM levels
WHERE level < 5)
SELECT level
FROM levels;
Loading