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);