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