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
172 changes: 133 additions & 39 deletions docs/queries.md

Large diffs are not rendered by default.

642 changes: 493 additions & 149 deletions sqlcj-cli/src/main/java/dev/sqlcj/analysis/QueryAnalyzer.java

Large diffs are not rendered by default.

551 changes: 545 additions & 6 deletions sqlcj-cli/src/test/java/dev/sqlcj/analysis/QueryAnalyzerTest.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3222,6 +3222,96 @@ INSERT INTO customer_orders (id, customer_id, quantity, status)
}
}

/**
* Covers derived sources end to end: a report over a left-joined common
* table expression and a read over a subquery source are generated,
* compiled, and executed against PostgreSQL, so the columns their bodies
* state are read through the generated result records, and an unmatched
* row reads the common table expression's column as {@code null}.
*/
@Test
void shouldExecuteGeneratedDerivedSourcesAgainstPostgres() throws Exception {
execute(CONSTRAINT_SCHEMA);

Path classesDirectory = generateAndCompile(
CONSTRAINT_SCHEMA,
"""
-- name: ListCustomerOrderTotals :many
WITH totals AS (SELECT customer_id, SUM(quantity)::int AS ordered
FROM customer_orders
WHERE quantity >= :minQuantity
GROUP BY customer_id)
SELECT c.name, totals.ordered
FROM customers c
LEFT JOIN totals ON totals.customer_id = c.id
ORDER BY c.name;

-- name: ListRecentCustomerOrders :many
SELECT recent.id, recent.quantity
FROM (SELECT id, quantity
FROM customer_orders
WHERE customer_id = :customerId
ORDER BY id DESC
LIMIT :pageSize) recent
ORDER BY recent.id;
"""
);

execute("INSERT INTO customers (id, name) VALUES (1, 'Alice'), (2, 'Bob')");

execute(
"""
INSERT INTO customer_orders (id, customer_id, quantity)
VALUES (10, 1, 3), (20, 1, 4), (30, 1, 1)
"""
);

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

Method totals = repository.getClass().getMethod(
"listCustomerOrderTotals",
Integer.class
);

List<?> reports = (List<?>) totals.invoke(repository, 2);

assertEquals(2, reports.size());

assertEquals(
List.of("name", "ordered"),
recordComponentNames(reports.get(0))
);

assertEquals("Alice", component(reports.get(0), "name"));
assertEquals(7, component(reports.get(0), "ordered"));

assertEquals("Bob", component(reports.get(1), "name"));
assertNull(component(reports.get(1), "ordered"));

Method recent = repository.getClass().getMethod(
"listRecentCustomerOrders",
Long.class,
Integer.class
);

List<?> orders = (List<?>) recent.invoke(repository, 1L, 2);

assertEquals(2, orders.size());

assertEquals(
List.of("id", "quantity"),
recordComponentNames(orders.get(0))
);

assertEquals(20L, component(orders.get(0), "id"));
assertEquals(4, component(orders.get(0), "quantity"));

assertEquals(30L, component(orders.get(1), "id"));
assertEquals(1, component(orders.get(1), "quantity"));
}
}

/**
* Covers a returning insert: the database-generated serial values and the
* target-table order of {@code RETURNING *} are read through the generated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,6 @@ class ReferenceServiceIntegrationTest {
"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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2437,18 +2437,25 @@ private static Stream<Arguments> rejectedQueries() {
),
Arguments.of(
":many",
"SELECT s.id FROM (SELECT id FROM users) s",
"A subquery in FROM is not supported."
"SELECT s.id FROM (SELECT id FROM users) AS s(n)",
"A column list on s is not supported; alias the columns in its SELECT instead."
),
Arguments.of(
":many",
"WITH recent AS (SELECT id FROM users) SELECT id FROM recent",
"A common table expression used as a source is not supported: recent"
"SELECT id FROM (SELECT id FROM users)",
"A subquery in FROM requires an alias, such as (SELECT ...) AS recent."
),
Arguments.of(
":many",
"WITH users AS (SELECT id FROM users) SELECT id FROM users",
"A common table expression used as a source is not supported: users"
"WITH RECURSIVE recent AS (SELECT id FROM users) SELECT id FROM recent",
"WITH RECURSIVE is not supported."
),
Arguments.of(
":many",
"WITH moved AS (DELETE FROM users WHERE id = $1 RETURNING id)"
+ " SELECT id FROM moved",
"A data-modifying statement in WITH is not supported;"
+ " run it as a query of its own."
),
Arguments.of(
":exec",
Expand Down Expand Up @@ -3858,6 +3865,64 @@ WHERE EXISTS (
);
}

/**
* A common table expression and a subquery source generate compilable Java
* whose result components carry the types their bodies state, with the
* columns of a left-joined derived source nullable.
*/
@Test
void shouldGenerateCompilableJavaForDerivedSources() throws IOException {
generateAndCompile(
JOIN_SCHEMA,
"""
-- name: ListUserOrderTotals :many
WITH totals AS (SELECT user_id, SUM(total)::numeric AS amount
FROM orders
WHERE total = $1
GROUP BY user_id)
SELECT u.name, totals.amount
FROM users u
LEFT JOIN totals ON totals.user_id = u.id;

-- name: ListRecentOrders :many
SELECT recent.id, recent.total
FROM (SELECT id, total
FROM orders
WHERE user_id = $1
ORDER BY id DESC
LIMIT $2) recent;
"""
);

String source = Files.readString(
tempDir.resolve("generated/generated/UsersRepository.java")
);

assertTrue(
source.contains(
"public List<ListUserOrderTotalsResult> listUserOrderTotals(BigDecimal total)"
),
source
);

assertEquals(
List.of("String name", "BigDecimal amount"),
recordComponents(source, "ListUserOrderTotalsResult")
);

assertTrue(
source.contains(
"public List<ListRecentOrdersResult> listRecentOrders(Long userId, Integer limit)"
),
source
);

assertEquals(
List.of("Long id", "BigDecimal total"),
recordComponents(source, "ListRecentOrdersResult")
);
}

/**
* A list predicate generates one {@code List} method parameter of the
* compared column's element type, bound as one server array, in compilable
Expand Down

This file was deleted.

This file was deleted.

Loading