Skip to content

Commit af0508e

Browse files
authored
build(server): upgrade to Spring Boot 4 (#20)
Upgrade the server module from Spring Boot 3.4.1 to 4.0.7 (Spring Framework 7, Jackson 3, Tomcat 11), the prerequisite for Spring AI 2.x MCP integration which requires Boot 4. Scoped to the server module: the engine stays Spring-free and on Java 17, and its tests plus the 20 sample queries are untouched. - bump spring-boot 3.4.1 to 4.0.7 and springdoc-openapi 2.7.0 to 3.0.3, the Boot-4 / Jackson-3 line - rename the deprecated spring-boot-starter-web dependency to spring-boot-starter-webmvc - move @WebMvcTest to org.springframework.boot.webmvc.test.autoconfigure and add spring-boot-starter-webmvc-test for the controller slice - restore TestRestTemplate, relocated to org.springframework.boot.resttestclient and no longer auto-configured, via @AutoConfigureTestRestTemplate plus the resttestclient and restclient dependencies - replace HttpHeaders.containsKey, removed in Framework 7, with containsHeader - move test JSON parsing to Jackson 3 (tools.jackson.databind, asString) so it no longer relies on a transitive Jackson 2 - keep the non-reparent BOM strategy so the parent and engine stay free of web-framework dependencies - sync the README and AGENTS server section to the live Spring Boot gateway
1 parent 3c67a8e commit af0508e

9 files changed

Lines changed: 78 additions & 39 deletions

File tree

AGENTS.md

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

3838
Per-file responsibilities are in the WHERE TO LOOK table below.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,9 @@ The test suite covers individual operators, the query planner, the optimiser, ex
273273
│ ├── db/data/ # CSV data files (header row + data rows)
274274
│ ├── input/query[1-20].sql # Sample queries
275275
│ └── expected_output/query[1-20].csv # Expected results
276-
├── server/ # REST gateway skeleton — Spring Boot REST gateway planned
277-
│ ├── pom.xml # cuckoodb-server (depends on cuckoodb-engine; no Spring yet)
278-
│ └── src/main/java/com/github/jinba1/cuckoodb/server/
276+
├── server/ # cuckoodb-server — Spring Boot REST gateway over the engine
277+
│ ├── pom.xml # Spring Boot 4 (web MVC), springdoc/OpenAPI
278+
│ └── src/main/java/com/github/jinba1/cuckoodb/server/ # controllers, query service, catalog facade, config
279279
├── mvnw / mvnw.cmd # Maven Wrapper
280280
└── LICENSE
281281
```

server/pom.xml

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
-->
2424

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

3033
<dependencyManagement>
@@ -47,7 +50,7 @@
4750
</dependency>
4851
<dependency>
4952
<groupId>org.springframework.boot</groupId>
50-
<artifactId>spring-boot-starter-web</artifactId>
53+
<artifactId>spring-boot-starter-webmvc</artifactId>
5154
</dependency>
5255
<dependency>
5356
<groupId>org.springdoc</groupId>
@@ -59,6 +62,34 @@
5962
<artifactId>spring-boot-starter-test</artifactId>
6063
<scope>test</scope>
6164
</dependency>
65+
<!--
66+
Boot 4 split the @WebMvcTest slice out of spring-boot-starter-test into a per-technology
67+
test starter; QueryControllerTest/TableControllerTest need it for the controller slice.
68+
-->
69+
<dependency>
70+
<groupId>org.springframework.boot</groupId>
71+
<artifactId>spring-boot-starter-webmvc-test</artifactId>
72+
<scope>test</scope>
73+
</dependency>
74+
<!--
75+
Boot 4 relocated TestRestTemplate/RestTestClient to org.springframework.boot.resttestclient
76+
and no longer auto-configures the client under @SpringBootTest. The 4 RANDOM_PORT integration
77+
tests keep TestRestTemplate (behaviour-preserving) via this dependency + @AutoConfigureTestRestTemplate.
78+
-->
79+
<dependency>
80+
<groupId>org.springframework.boot</groupId>
81+
<artifactId>spring-boot-resttestclient</artifactId>
82+
<scope>test</scope>
83+
</dependency>
84+
<!--
85+
TestRestTemplate's auto-config needs org.springframework.boot.restclient.RestTemplateBuilder,
86+
which spring-boot-resttestclient does not pull transitively (Boot #48588) — declare it explicitly.
87+
-->
88+
<dependency>
89+
<groupId>org.springframework.boot</groupId>
90+
<artifactId>spring-boot-restclient</artifactId>
91+
<scope>test</scope>
92+
</dependency>
6293
</dependencies>
6394

6495
<build>

server/src/test/java/com/github/jinba1/cuckoodb/server/web/BudgetIntegrationTest.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import static org.junit.jupiter.api.Assertions.assertEquals;
55
import static org.junit.jupiter.api.Assertions.assertTrue;
66

7-
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import tools.jackson.databind.ObjectMapper;
88

99
import java.io.IOException;
1010
import java.nio.file.Files;
@@ -14,7 +14,8 @@
1414
import org.junit.jupiter.api.Test;
1515
import org.springframework.beans.factory.annotation.Autowired;
1616
import org.springframework.boot.test.context.SpringBootTest;
17-
import org.springframework.boot.test.web.client.TestRestTemplate;
17+
import org.springframework.boot.resttestclient.TestRestTemplate;
18+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
1819
import org.springframework.http.HttpEntity;
1920
import org.springframework.http.HttpHeaders;
2021
import org.springframework.http.MediaType;
@@ -31,6 +32,7 @@
3132
*/
3233
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
3334
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
35+
@AutoConfigureTestRestTemplate
3436
class BudgetIntegrationTest {
3537

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

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

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

9193
private ResponseEntity<String> postQuery(String json) {

server/src/test/java/com/github/jinba1/cuckoodb/server/web/QueryApiIntegrationTest.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import static org.junit.jupiter.api.Assertions.assertNotNull;
77
import static org.junit.jupiter.api.Assertions.assertTrue;
88

9-
import com.fasterxml.jackson.databind.JsonNode;
10-
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import tools.jackson.databind.JsonNode;
10+
import tools.jackson.databind.ObjectMapper;
1111

1212
import java.io.IOException;
1313
import java.nio.file.Files;
@@ -23,7 +23,8 @@
2323
import org.junit.jupiter.api.Test;
2424
import org.springframework.beans.factory.annotation.Autowired;
2525
import org.springframework.boot.test.context.SpringBootTest;
26-
import org.springframework.boot.test.web.client.TestRestTemplate;
26+
import org.springframework.boot.resttestclient.TestRestTemplate;
27+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
2728
import org.springframework.http.HttpEntity;
2829
import org.springframework.http.HttpHeaders;
2930
import org.springframework.http.HttpMethod;
@@ -42,6 +43,7 @@
4243
*/
4344
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
4445
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
46+
@AutoConfigureTestRestTemplate
4547
class QueryApiIntegrationTest {
4648

4749
private static final ObjectMapper JSON = new ObjectMapper();
@@ -81,11 +83,11 @@ void syncQueryRoundTripReturnsTypedColumnArrays() throws Exception {
8183
JsonNode body = postQuery("{\"sql\":\"SELECT * FROM People\"}", 200);
8284
assertEquals(3, body.get("rowCount").asInt());
8385
assertFalse(body.get("truncated").asBoolean());
84-
assertEquals("id", body.get("columns").get(0).get("name").asText());
85-
assertEquals("INT", body.get("columns").get(0).get("type").asText());
86-
assertEquals("STRING", body.get("columns").get(1).get("type").asText());
86+
assertEquals("id", body.get("columns").get(0).get("name").asString());
87+
assertEquals("INT", body.get("columns").get(0).get("type").asString());
88+
assertEquals("STRING", body.get("columns").get(1).get("type").asString());
8789
assertEquals(1, body.get("rows").get(0).get(0).asInt());
88-
assertEquals("alice", body.get("rows").get(0).get(1).asText());
90+
assertEquals("alice", body.get("rows").get(0).get(1).asString());
8991
}
9092

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

107109
@Test
108110
void unknownTableInQueryBodyIs422() throws Exception {
109111
JsonNode body = postQuery("{\"sql\":\"SELECT * FROM Nope\"}", 422);
110-
assertEquals("UNKNOWN_TABLE", body.get("errorCode").asText());
112+
assertEquals("UNKNOWN_TABLE", body.get("errorCode").asString());
111113
}
112114

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

130132
@Test

server/src/test/java/com/github/jinba1/cuckoodb/server/web/QueryControllerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import org.junit.jupiter.api.BeforeEach;
3232
import org.junit.jupiter.api.Test;
3333
import org.springframework.beans.factory.annotation.Autowired;
34-
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
34+
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
3535
import org.springframework.http.MediaType;
3636
import org.springframework.test.context.bean.override.mockito.MockitoBean;
3737
import org.springframework.test.web.servlet.MockMvc;

server/src/test/java/com/github/jinba1/cuckoodb/server/web/TableControllerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import org.junit.jupiter.api.Test;
1616
import org.springframework.beans.factory.annotation.Autowired;
17-
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
17+
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
1818
import org.springframework.test.context.bean.override.mockito.MockitoBean;
1919
import org.springframework.test.web.servlet.MockMvc;
2020

server/src/test/java/com/github/jinba1/cuckoodb/server/web/UploadApiIntegrationTest.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import static org.junit.jupiter.api.Assertions.assertFalse;
66
import static org.junit.jupiter.api.Assertions.assertNotNull;
77

8-
import com.fasterxml.jackson.databind.JsonNode;
9-
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import tools.jackson.databind.JsonNode;
9+
import tools.jackson.databind.ObjectMapper;
1010

1111
import java.io.IOException;
1212
import java.nio.file.Files;
@@ -16,7 +16,8 @@
1616
import org.junit.jupiter.api.Test;
1717
import org.springframework.beans.factory.annotation.Autowired;
1818
import org.springframework.boot.test.context.SpringBootTest;
19-
import org.springframework.boot.test.web.client.TestRestTemplate;
19+
import org.springframework.boot.resttestclient.TestRestTemplate;
20+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
2021
import org.springframework.http.HttpEntity;
2122
import org.springframework.http.HttpHeaders;
2223
import org.springframework.http.HttpMethod;
@@ -33,6 +34,7 @@
3334
*/
3435
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
3536
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
37+
@AutoConfigureTestRestTemplate
3638
class UploadApiIntegrationTest {
3739

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

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

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

147149
private ResponseEntity<String> csv(String path, String body) {

server/src/test/java/com/github/jinba1/cuckoodb/server/web/UploadLimitIntegrationTest.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import static org.junit.jupiter.api.Assertions.assertEquals;
55
import static org.junit.jupiter.api.Assertions.assertFalse;
66

7-
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import tools.jackson.databind.ObjectMapper;
88

99
import java.io.IOException;
1010
import java.nio.file.Files;
@@ -14,7 +14,8 @@
1414
import org.junit.jupiter.api.Test;
1515
import org.springframework.beans.factory.annotation.Autowired;
1616
import org.springframework.boot.test.context.SpringBootTest;
17-
import org.springframework.boot.test.web.client.TestRestTemplate;
17+
import org.springframework.boot.resttestclient.TestRestTemplate;
18+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
1819
import org.springframework.http.HttpEntity;
1920
import org.springframework.http.HttpHeaders;
2021
import org.springframework.http.MediaType;
@@ -29,6 +30,7 @@
2930
*/
3031
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
3132
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
33+
@AutoConfigureTestRestTemplate
3234
class UploadLimitIntegrationTest {
3335

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

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

7274
private ResponseEntity<String> csv(String path, String body) {

0 commit comments

Comments
 (0)