From 26af914af4b9e1d90f7c13d61a7a3597bf66bd0f Mon Sep 17 00:00:00 2001
From: JinBa1 <72070041+JinBa1@users.noreply.github.com>
Date: Fri, 19 Jun 2026 23:47:45 +0100
Subject: [PATCH] build(server): upgrade to Spring Boot 4.0.7 (Jackson 3,
Framework 7)
Platform upgrade prerequisite for the Spring AI 2.x MCP layer. Server module
only; the engine stays Spring-free and Java 17, and its 419 tests + the 20
byte-identical sample queries are untouched.
- spring-boot 3.4.1 -> 4.0.7, springdoc 2.7.0 -> 3.0.3 (Boot-4/Jackson-3 line)
- spring-boot-starter-web -> spring-boot-starter-webmvc (deprecated rename)
- @WebMvcTest moved to org.springframework.boot.webmvc.test.autoconfigure;
add spring-boot-starter-webmvc-test
- TestRestTemplate relocated to org.springframework.boot.resttestclient and no
longer auto-configured: add the dependency + @AutoConfigureTestRestTemplate,
plus spring-boot-restclient (Boot #48588, not pulled transitively)
- HttpHeaders.containsKey() removed in Framework 7 -> containsHeader()
- migrate test JSON parsing to Jackson 3 (tools.jackson.databind, asString())
so it no longer relies on a transitive Jackson 2 from springdoc
- sync README/AGENTS server section to the live Spring Boot gateway (was stale
'no Spring yet / ServerPlaceholder' since the gateway landed)
Non-reparent BOM strategy retained; Java baseline stays 17. All 52 server
tests green (incl. the 4 RANDOM_PORT integration tests over real HTTP).
---
AGENTS.md | 6 +--
README.md | 6 +--
server/pom.xml | 37 +++++++++++++++++--
.../server/web/BudgetIntegrationTest.java | 14 ++++---
.../server/web/QueryApiIntegrationTest.java | 22 ++++++-----
.../server/web/QueryControllerTest.java | 2 +-
.../server/web/TableControllerTest.java | 2 +-
.../server/web/UploadApiIntegrationTest.java | 18 +++++----
.../web/UploadLimitIntegrationTest.java | 10 +++--
9 files changed, 78 insertions(+), 39 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 18d8bd9..eabbf20 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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.
diff --git a/README.md b/README.md
index 20e4041..36e2188 100644
--- a/README.md
+++ b/README.md
@@ -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
```
diff --git a/server/pom.xml b/server/pom.xml
index 9433ad2..2a2a6db 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -23,8 +23,11 @@
-->
- 3.4.1
- 2.7.0
+ 4.0.7
+
+ 3.0.3
@@ -47,7 +50,7 @@
org.springframework.boot
- spring-boot-starter-web
+ spring-boot-starter-webmvc
org.springdoc
@@ -59,6 +62,34 @@
spring-boot-starter-test
test
+
+
+ org.springframework.boot
+ spring-boot-starter-webmvc-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-resttestclient
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-restclient
+ test
+
diff --git a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/BudgetIntegrationTest.java b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/BudgetIntegrationTest.java
index d240f20..b03508f 100644
--- a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/BudgetIntegrationTest.java
+++ b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/BudgetIntegrationTest.java
@@ -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;
@@ -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;
@@ -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();
@@ -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 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
@@ -77,7 +79,7 @@ void hugeClientBudgetIsClampedToCapAndStillTrips() throws Exception {
ResponseEntity 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
@@ -85,7 +87,7 @@ void explainConsumesNoBudget() throws Exception {
// EXPLAIN performs no execution, so the tiny budget never bites: a plan comes back 200.
ResponseEntity 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 postQuery(String json) {
diff --git a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/QueryApiIntegrationTest.java b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/QueryApiIntegrationTest.java
index 3821d06..64b51c0 100644
--- a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/QueryApiIntegrationTest.java
+++ b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/QueryApiIntegrationTest.java
@@ -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;
@@ -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;
@@ -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();
@@ -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
@@ -99,7 +101,7 @@ 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"));
}
@@ -107,7 +109,7 @@ void explainReturnsPlanWithNoRowsAndNoRowCount() throws Exception {
@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
@@ -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
diff --git a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/QueryControllerTest.java b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/QueryControllerTest.java
index 2316f1b..7e9ee1d 100644
--- a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/QueryControllerTest.java
+++ b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/QueryControllerTest.java
@@ -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;
diff --git a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/TableControllerTest.java b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/TableControllerTest.java
index b52dfab..b9d4c5a 100644
--- a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/TableControllerTest.java
+++ b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/TableControllerTest.java
@@ -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;
diff --git a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/UploadApiIntegrationTest.java b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/UploadApiIntegrationTest.java
index 78f6872..0412f0a 100644
--- a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/UploadApiIntegrationTest.java
+++ b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/UploadApiIntegrationTest.java
@@ -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;
@@ -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;
@@ -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();
@@ -67,10 +69,10 @@ void uploadThenQueryRoundTrips() throws Exception {
ResponseEntity 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 query = exchange("/queries", HttpMethod.POST,
@@ -82,7 +84,7 @@ void uploadThenQueryRoundTrips() throws Exception {
ResponseEntity 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
@@ -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 csv(String path, String body) {
diff --git a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/UploadLimitIntegrationTest.java b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/UploadLimitIntegrationTest.java
index 3b2c52d..3cab925 100644
--- a/server/src/test/java/com/github/jinba1/cuckoodb/server/web/UploadLimitIntegrationTest.java
+++ b/server/src/test/java/com/github/jinba1/cuckoodb/server/web/UploadLimitIntegrationTest.java
@@ -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;
@@ -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;
@@ -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();
@@ -64,9 +66,9 @@ void uploadOverTableCapReturns507() throws Exception {
ResponseEntity 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 csv(String path, String body) {