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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Build
run: ./mvnw clean compile
- name: Test
run: ./mvnw test
- name: Build, test and package
# package subsumes compile + test for both modules and exercises the
# spring-boot-maven-plugin repackage, so a broken boot jar can't ship.
run: ./mvnw clean package
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
Expand Down
4 changes: 4 additions & 0 deletions engine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,25 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
15 changes: 15 additions & 0 deletions engine/src/main/java/com/github/jinba1/cuckoodb/BudgetKind.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.jinba1.cuckoodb;

/**
* Which limit of a {@link QueryBudget} was breached. The engine throws one
* {@link QueryBudgetExceededException} type for both, but downstream surfaces treat them
* differently: a network gateway maps a {@link #TUPLES} breach to "retry with a narrower
* scope" (429) and a {@link #TIME} breach to "the query took too long" (504). Carried as a
* field so consumers classify by enum, never by parsing the message.
*/
public enum BudgetKind {
/** The tuple-count limit was exceeded. */
TUPLES,
/** The wall-clock time limit was exceeded. */
TIME
}
22 changes: 22 additions & 0 deletions engine/src/main/java/com/github/jinba1/cuckoodb/CsvFormats.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.github.jinba1.cuckoodb;

import org.apache.commons.csv.CSVFormat;

/**
* The single CSV dialect every table reader must use, so the catalog's parse-and-infer, the
* {@code ScanOperator}'s row scan, and any external row count all tokenize a file identically.
* RFC4180 with surrounding spaces ignored — keeping these in one constant prevents the readers
* from drifting (e.g. a leading space before a quoted, embedded-newline field changes the record
* count between {@code ignoreSurroundingSpaces} and plain RFC4180). {@link CSVFormat} is immutable
* and thread-safe, so this shared instance is safe to reuse across threads and parsers.
*/
public final class CsvFormats {

private CsvFormats() {
}

/** The dialect for every base-table CSV: RFC4180, surrounding spaces ignored. */
public static final CSVFormat TABLE = CSVFormat.RFC4180.builder()
.setIgnoreSurroundingSpaces(true)
.build();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.jinba1.cuckoodb;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;

Expand Down Expand Up @@ -139,10 +138,7 @@ public boolean registerTable(String tableName, Path csv) {
* an all-INT table until rows arrive.
*/
private static TableMeta parseTable(String tableName, Path csv) throws IOException {
CSVFormat format = CSVFormat.RFC4180.builder()
.setIgnoreSurroundingSpaces(true)
.build();
try (CSVParser parser = CSVParser.parse(csv, StandardCharsets.UTF_8, format)) {
try (CSVParser parser = CSVParser.parse(csv, StandardCharsets.UTF_8, CsvFormats.TABLE)) {
Iterator<CSVRecord> it = parser.iterator();
if (!it.hasNext()) {
throw new QueryExecutionException(ErrorCode.DATA_ERROR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ public void charge() {
}
processed++;
if (maxTuples != null && processed > maxTuples) {
throw new QueryBudgetExceededException(
throw new QueryBudgetExceededException(BudgetKind.TUPLES,
"Tuple budget exceeded: limit " + maxTuples + ", query processed "
+ processed + " tuples");
}
// >= so an already-expired deadline (timeout 0) trips regardless of clock resolution
if (timeoutMs != null && System.nanoTime() >= deadlineNanos) {
throw new QueryBudgetExceededException(
throw new QueryBudgetExceededException(BudgetKind.TIME,
"Time budget exceeded: limit " + timeoutMs + " ms");
}
}
Expand All @@ -67,7 +67,7 @@ public void checkDeadline() {
deadlineNanos = System.nanoTime() + timeoutMs * 1_000_000;
}
if (System.nanoTime() >= deadlineNanos) {
throw new QueryBudgetExceededException(
throw new QueryBudgetExceededException(BudgetKind.TIME,
"Time budget exceeded: limit " + timeoutMs + " ms");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@
* Subclass of QueryExecutionException so existing error handling paths apply.
*/
public class QueryBudgetExceededException extends QueryExecutionException {

private final BudgetKind kind;

/**
* @param kind which limit was breached (tuples vs time); lets a gateway map the same
* engine exception to different HTTP statuses without parsing the message
* @param message which limit was exceeded and by how much; the error code is
* always {@link ErrorCode#BUDGET_EXCEEDED}
*/
public QueryBudgetExceededException(String message) {
public QueryBudgetExceededException(BudgetKind kind, String message) {
super(ErrorCode.BUDGET_EXCEEDED, message);
this.kind = kind;
}

/** Which budget limit was breached. */
public BudgetKind kind() {
return kind;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.jinba1.cuckoodb.operator;

import com.github.jinba1.cuckoodb.ColumnType;
import com.github.jinba1.cuckoodb.CsvFormats;
import com.github.jinba1.cuckoodb.DBCatalog;
import com.github.jinba1.cuckoodb.ErrorCode;
import com.github.jinba1.cuckoodb.IntValue;
Expand All @@ -11,7 +12,6 @@
import com.github.jinba1.cuckoodb.Tuple;
import com.github.jinba1.cuckoodb.Value;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;

Expand Down Expand Up @@ -65,10 +65,7 @@ public ScanOperator(PlanContext ctx, String tableName) {
*/
private void openReader() {
try {
CSVFormat format = CSVFormat.RFC4180.builder()
.setIgnoreSurroundingSpaces(true)
.build();
parser = CSVParser.parse(tablePath, StandardCharsets.UTF_8, format);
parser = CSVParser.parse(tablePath, StandardCharsets.UTF_8, CsvFormats.TABLE);
records = parser.iterator();
if (records.hasNext()) {
records.next(); // skip header row
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,32 @@ public void exceptionIsAQueryExecutionException() {
assertTrue(QueryExecutionException.class.isAssignableFrom(QueryBudgetExceededException.class));
}

@Test
public void tupleBreachReportsKindTuples() {
// The REST layer maps a TUPLES breach to 429 and a TIME breach to 504, so the kind
// must be programmatically distinguishable, not parsed from the message text.
QueryBudget budget = new QueryBudget(0L, null);
QueryBudgetExceededException ex =
assertThrows(QueryBudgetExceededException.class, budget::charge);
assertEquals(BudgetKind.TUPLES, ex.kind());
}

@Test
public void timeBreachInChargeReportsKindTime() {
QueryBudget budget = new QueryBudget(null, 0L);
QueryBudgetExceededException ex =
assertThrows(QueryBudgetExceededException.class, budget::charge);
assertEquals(BudgetKind.TIME, ex.kind());
}

@Test
public void timeBreachInCheckDeadlineReportsKindTime() {
QueryBudget budget = new QueryBudget(null, 0L);
QueryBudgetExceededException ex =
assertThrows(QueryBudgetExceededException.class, budget::checkDeadline);
assertEquals(BudgetKind.TIME, ex.kind());
}

@Test
public void checkDeadlineWithZeroTimeoutTripsImmediately() {
QueryBudget budget = new QueryBudget(null, 0L);
Expand Down
26 changes: 6 additions & 20 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,14 @@
<jmh.version>1.37</jmh.version>
</properties>

<!--
Shared dependency versions. JUnit and JMH are intentionally NOT managed here: the engine
pins them itself (Spring-free, JUnit 5.10.2), while the server imports the spring-boot
BOM and must take its JUnit from there. Managing JUnit centrally would force the server's
JUnit platform/jupiter versions out of sync with the Spring BOM and break test discovery.
-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
Expand All @@ -46,16 +42,6 @@
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down
67 changes: 64 additions & 3 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,77 @@

<name>cuckooDB Server</name>
<!--
Skeleton only. Spring Boot and the REST gateway are added in a later change.
The single dependency on cuckoodb-engine proves the reactor wiring and the
server -> engine direction; no Spring / web dependency enters yet.
Spring Boot REST gateway over the engine. Spring enters here ONLY: the parent POM and the
engine module stay Spring-free. We import the spring-boot-dependencies BOM rather than
reparenting to spring-boot-starter-parent, so the module keeps cuckoodb-parent and the
engine/server seam stays structurally enforced.
-->

<properties>
<spring-boot.version>3.4.1</spring-boot.version>
<springdoc.version>2.7.0</springdoc.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>com.github.jinba1</groupId>
<artifactId>cuckoodb-engine</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!--
Spring MVC needs parameter names at runtime (e.g. @PathVariable {name} -> method
arg). spring-boot-starter-parent adds this automatically; since we deliberately do
NOT reparent (keeping cuckoodb-parent), we enable it here ourselves.
-->
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.github.jinba1.cuckoodb.server;

import com.github.jinba1.cuckoodb.server.config.CuckooDbProperties;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

/**
* Entry point for the cuckooDB REST gateway: a guarded, read-only-by-construction,
* resource-budgeted HTTP front door onto the in-memory query engine.
*/
@SpringBootApplication
@EnableConfigurationProperties(CuckooDbProperties.class)
public class CuckooDbServerApplication {

public static void main(String[] args) {
SpringApplication.run(CuckooDbServerApplication.class, args);
}
}

This file was deleted.

Loading
Loading