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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ cuckoodb-parent/ # repo root (git slug: java-query-engine)
│ ├── ConcurrentQueryExecutionTest.java, DBCatalogTest.java, ... # planner / optimizer / budget / EXPLAIN / end-to-end
│ ├── operator/ # operator-level tests + CachedOperator test utility
│ └── bench/ # JMH benchmarks (compiled in CI, never run there): EndToEndJoinBenchmark, JoinAlgorithmBenchmark
└── server/ # cuckoodb-server — REST gateway skeleton; Spring Boot REST gateway planned
├── pom.xml # depends on cuckoodb-engine (${project.version}); NO Spring yet
└── src/main/java/com/github/jinba1/cuckoodb/server/ServerPlaceholder.java # compile-links an engine type to prove reactor wiring
└── server/ # cuckoodb-server — Spring Boot 4 REST gateway over the engine
├── pom.xml # depends on cuckoodb-engine; Spring Boot 4.0.7 (web MVC), springdoc/OpenAPI
└── src/main/java/com/github/jinba1/cuckoodb/server/ # web/ controllers + GlobalExceptionHandler, query/ QueryService+budget+concurrency, catalog/ CatalogFacade, audit/ sink, config/ (52 server tests)
```

Per-file responsibilities are in the WHERE TO LOOK table below.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,9 @@ The test suite covers individual operators, the query planner, the optimiser, ex
│ ├── db/data/ # CSV data files (header row + data rows)
│ ├── input/query[1-20].sql # Sample queries
│ └── expected_output/query[1-20].csv # Expected results
├── server/ # REST gateway skeleton — Spring Boot REST gateway planned
│ ├── pom.xml # cuckoodb-server (depends on cuckoodb-engine; no Spring yet)
│ └── src/main/java/com/github/jinba1/cuckoodb/server/
├── server/ # cuckoodb-server — Spring Boot REST gateway over the engine
│ ├── pom.xml # Spring Boot 4 (web MVC), springdoc/OpenAPI
│ └── src/main/java/com/github/jinba1/cuckoodb/server/ # controllers, query service, catalog facade, config
├── mvnw / mvnw.cmd # Maven Wrapper
└── LICENSE
```
Expand Down
37 changes: 34 additions & 3 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
-->

<properties>
<spring-boot.version>3.4.1</spring-boot.version>
<springdoc.version>2.7.0</springdoc.version>
<spring-boot.version>4.0.7</spring-boot.version>
<!-- springdoc 3.0.3 is the Boot-4/Jackson-3 line; verified compatible with Boot 4.0.7
(full @SpringBootTest contexts load with springdoc auto-config; no MVC API-versioning,
so springdoc #3163 does not apply). -->
<springdoc.version>3.0.3</springdoc.version>
</properties>

<dependencyManagement>
Expand All @@ -47,7 +50,7 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
Expand All @@ -59,6 +62,34 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--
Boot 4 split the @WebMvcTest slice out of spring-boot-starter-test into a per-technology
test starter; QueryControllerTest/TableControllerTest need it for the controller slice.
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<!--
Boot 4 relocated TestRestTemplate/RestTestClient to org.springframework.boot.resttestclient
and no longer auto-configures the client under @SpringBootTest. The 4 RANDOM_PORT integration
tests keep TestRestTemplate (behaviour-preserving) via this dependency + @AutoConfigureTestRestTemplate.
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-resttestclient</artifactId>
<scope>test</scope>
</dependency>
<!--
TestRestTemplate's auto-config needs org.springframework.boot.restclient.RestTemplateBuilder,
which spring-boot-resttestclient does not pull transitively (Boot #48588) — declare it explicitly.
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-restclient</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.nio.file.Files;
Expand All @@ -14,7 +14,8 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.resttestclient.TestRestTemplate;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
Expand All @@ -31,6 +32,7 @@
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@AutoConfigureTestRestTemplate
class BudgetIntegrationTest {

private static final ObjectMapper JSON = new ObjectMapper();
Expand Down Expand Up @@ -67,8 +69,8 @@ void omittedBudgetStillEnforcesTheDefault() throws Exception {
// which a 4-row scan exceeds. A 200 here would mean the unbounded path was reachable.
ResponseEntity<String> resp = postQuery("{\"sql\":\"SELECT * FROM People\"}");
assertEquals(429, resp.getStatusCode().value(), resp.getBody());
assertEquals("BUDGET_EXCEEDED", JSON.readTree(resp.getBody()).get("errorCode").asText());
assertTrue(resp.getHeaders().containsKey("Retry-After"));
assertEquals("BUDGET_EXCEEDED", JSON.readTree(resp.getBody()).get("errorCode").asString());
assertTrue(resp.getHeaders().containsHeader("Retry-After"));
}

@Test
Expand All @@ -77,15 +79,15 @@ void hugeClientBudgetIsClampedToCapAndStillTrips() throws Exception {
ResponseEntity<String> resp = postQuery(
"{\"sql\":\"SELECT * FROM People\",\"maxTuples\":9999999}");
assertEquals(429, resp.getStatusCode().value(), resp.getBody());
assertEquals("BUDGET_EXCEEDED", JSON.readTree(resp.getBody()).get("errorCode").asText());
assertEquals("BUDGET_EXCEEDED", JSON.readTree(resp.getBody()).get("errorCode").asString());
}

@Test
void explainConsumesNoBudget() throws Exception {
// EXPLAIN performs no execution, so the tiny budget never bites: a plan comes back 200.
ResponseEntity<String> resp = postQuery("{\"sql\":\"EXPLAIN SELECT * FROM People\"}");
assertEquals(200, resp.getStatusCode().value(), resp.getBody());
assertTrue(JSON.readTree(resp.getBody()).get("explain").asText().contains("Plan"));
assertTrue(JSON.readTree(resp.getBody()).get("explain").asString().contains("Plan"));
}

private ResponseEntity<String> postQuery(String json) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.nio.file.Files;
Expand All @@ -23,7 +23,8 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.resttestclient.TestRestTemplate;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
Expand All @@ -42,6 +43,7 @@
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@AutoConfigureTestRestTemplate
class QueryApiIntegrationTest {

private static final ObjectMapper JSON = new ObjectMapper();
Expand Down Expand Up @@ -81,11 +83,11 @@ void syncQueryRoundTripReturnsTypedColumnArrays() throws Exception {
JsonNode body = postQuery("{\"sql\":\"SELECT * FROM People\"}", 200);
assertEquals(3, body.get("rowCount").asInt());
assertFalse(body.get("truncated").asBoolean());
assertEquals("id", body.get("columns").get(0).get("name").asText());
assertEquals("INT", body.get("columns").get(0).get("type").asText());
assertEquals("STRING", body.get("columns").get(1).get("type").asText());
assertEquals("id", body.get("columns").get(0).get("name").asString());
assertEquals("INT", body.get("columns").get(0).get("type").asString());
assertEquals("STRING", body.get("columns").get(1).get("type").asString());
assertEquals(1, body.get("rows").get(0).get(0).asInt());
assertEquals("alice", body.get("rows").get(0).get(1).asText());
assertEquals("alice", body.get("rows").get(0).get(1).asString());
}

@Test
Expand All @@ -99,15 +101,15 @@ void limitMarksResultTruncated() throws Exception {
@Test
void explainReturnsPlanWithNoRowsAndNoRowCount() throws Exception {
JsonNode body = postQuery("{\"sql\":\"EXPLAIN SELECT * FROM People\"}", 200);
assertTrue(body.get("explain").asText().contains("Plan (as written)"));
assertTrue(body.get("explain").asString().contains("Plan (as written)"));
assertFalse(body.has("rows"), "EXPLAIN performs no execution, so rows is absent");
assertFalse(body.has("rowCount"));
}

@Test
void unknownTableInQueryBodyIs422() throws Exception {
JsonNode body = postQuery("{\"sql\":\"SELECT * FROM Nope\"}", 422);
assertEquals("UNKNOWN_TABLE", body.get("errorCode").asText());
assertEquals("UNKNOWN_TABLE", body.get("errorCode").asString());
}

@Test
Expand All @@ -124,7 +126,7 @@ void queryPathDataErrorReturns500WithNoFilesystemPath() throws Exception {
assertFalse(raw.contains(dataDir.toString()), "5xx body leaked a filesystem path: " + raw);
JsonNode body = JSON.readTree(raw);
assertNotNull(body.get("errorId"), "5xx carries a correlation id");
assertEquals("Internal server error.", body.get("message").asText());
assertEquals("Internal server error.", body.get("message").asString());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.nio.file.Files;
Expand All @@ -16,7 +16,8 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.resttestclient.TestRestTemplate;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
Expand All @@ -33,6 +34,7 @@
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@AutoConfigureTestRestTemplate
class UploadApiIntegrationTest {

private static final ObjectMapper JSON = new ObjectMapper();
Expand Down Expand Up @@ -67,10 +69,10 @@ void uploadThenQueryRoundTrips() throws Exception {
ResponseEntity<String> upload = csv("/tables/Widgets", "sku,qty\nA,5\nB,7\n");
assertEquals(201, upload.getStatusCode().value(), upload.getBody());
JsonNode created = JSON.readTree(upload.getBody());
assertEquals("Widgets", created.get("name").asText());
assertEquals("Widgets", created.get("name").asString());
assertEquals(2, created.get("rowCount").asInt());
assertEquals("qty", created.get("columns").get(1).get("name").asText());
assertEquals("INT", created.get("columns").get(1).get("type").asText());
assertEquals("qty", created.get("columns").get(1).get("name").asString());
assertEquals("INT", created.get("columns").get(1).get("type").asString());

// The freshly uploaded table is immediately queryable.
ResponseEntity<String> query = exchange("/queries", HttpMethod.POST,
Expand All @@ -82,7 +84,7 @@ void uploadThenQueryRoundTrips() throws Exception {
ResponseEntity<String> describe = rest.getForEntity("/tables/Widgets", String.class);
assertEquals(200, describe.getStatusCode().value());
assertEquals("STRING", JSON.readTree(describe.getBody())
.get("columns").get(0).get("type").asText(), "sku column is string-typed");
.get("columns").get(0).get("type").asString(), "sku column is string-typed");
}

@Test
Expand Down Expand Up @@ -141,7 +143,7 @@ void malformedCsvReturns400WithNoFilesystemPath() throws Exception {
String raw = resp.getBody();
assertNotNull(raw);
assertFalse(raw.contains(workDir.toString()), "upload 400 leaked a work-dir path: " + raw);
assertEquals("DATA_ERROR", JSON.readTree(raw).get("errorCode").asText());
assertEquals("DATA_ERROR", JSON.readTree(raw).get("errorCode").asString());
}

private ResponseEntity<String> csv(String path, String body) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.nio.file.Files;
Expand All @@ -14,7 +14,8 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.resttestclient.TestRestTemplate;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
Expand All @@ -29,6 +30,7 @@
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@AutoConfigureTestRestTemplate
class UploadLimitIntegrationTest {

private static final ObjectMapper JSON = new ObjectMapper();
Expand Down Expand Up @@ -64,9 +66,9 @@ void uploadOverTableCapReturns507() throws Exception {

ResponseEntity<String> overCap = csv("/tables/Second", "x\n1\n");
assertEquals(507, overCap.getStatusCode().value(), overCap.getBody());
assertEquals("TABLE_LIMIT", JSON.readTree(overCap.getBody()).get("errorCode").asText());
assertEquals("TABLE_LIMIT", JSON.readTree(overCap.getBody()).get("errorCode").asString());
// No eviction: 507 is not retryable, so it carries no Retry-After.
assertFalse(overCap.getHeaders().containsKey("Retry-After"));
assertFalse(overCap.getHeaders().containsHeader("Retry-After"));
}

private ResponseEntity<String> csv(String path, String body) {
Expand Down
Loading