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
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,41 @@ jobs:

- name: Verify release artifacts
run: mvn -B -Prelease -Dgpg.skip=true verify

examples:
name: Compile and run examples
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Set up Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5
with:
distribution: temurin
java-version: "17"
cache: maven

- name: Install current library
run: mvn -B -DskipTests install

- name: Read current project version
id: project-version
run: echo "version=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)" >> "$GITHUB_OUTPUT"

- name: Compile examples
run: >-
mvn -B -f examples/quickstart/pom.xml
-Dcloudflare-d1-java.version=${{ steps.project-version.outputs.version }}
compile

- name: Run credential-free custom transport example
run: >-
mvn -B -f examples/quickstart/pom.xml
-Dcloudflare-d1-java.version=${{ steps.project-version.outputs.version }}
exec:java
-Dexec.mainClass=example.CustomTransportExample
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,36 @@ This project follows Semantic Versioning.

## [Unreleased]

### Added

- Added `D1RawBatchException` with the first failed item index and immutable partial raw results for
item-level raw batch failures.
- Added `D1Client.builderFromEnv()` and `D1AsyncClient.builderFromEnv()` so standard environment
credentials can be combined with timeout, retry, transport, mapping, and executor configuration.

### Changed

- Made the quickstart dependency version overridable and added Java 17 CI coverage that compiles
all examples against the current project artifact and runs the credential-free custom transport
example.

### Fixed

- Made nested JSON maps and lists in public response and mapping-error models recursively immutable
and defensively copied.
- Invalid JSON responses with successful HTTP status codes now retain the attempted operation in
exception metadata.
- `D1AsyncClient.close()` now allows accepted queued and in-flight operations to finish before
closing the transport, rejects all post-close operations consistently, and leaves caller-supplied
executors open.
- Preserved positive sub-millisecond default transport timeouts by rounding them up to one
millisecond, while keeping zero unlimited and safely capping oversized durations.
- Added the missing Maven `compile` phase to parameterized quickstart commands.

### Documentation

- Added environment-backed builder, Gradle Kotlin DSL, PowerShell, and timeout behavior guidance.

## [0.2.0] - 2026-07-11

See the [migration guide](docs/guides/migration-0.2.md) for upgrade steps from 0.1.x.
Expand Down
57 changes: 51 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,18 @@ Use the Workers D1 binding for code running inside Cloudflare Workers. Use a Wor
</dependency>
```

Gradle:
Gradle Groovy DSL:

```groovy
implementation "io.github.xxvw:cloudflare-d1-java:0.2.0"
```

Gradle Kotlin DSL:

```kotlin
implementation("io.github.xxvw:cloudflare-d1-java:0.2.0")
```

Requirements:

- Java 8 or newer.
Expand Down Expand Up @@ -75,6 +81,20 @@ set -a
set +a
```

PowerShell:

```powershell
Copy-Item .env.example .env
notepad .env

Get-Content .env | ForEach-Object {
if ($_ -match '^\s*([^#][^=]*)=(.*)$') {
[Environment]::SetEnvironmentVariable(
$matches[1].Trim(), $matches[2].Trim(), "Process")
}
}
```

The `.env` file is ignored by Git. Never commit real Cloudflare account IDs, database IDs, or API
tokens.

Expand All @@ -90,6 +110,22 @@ try (D1Client d1 = D1Client.fromEnv()) {
}
```

Use `builderFromEnv()` when you want the same environment variable loading with additional
configuration:

```java
try (D1Client d1 = D1Client.builderFromEnv()
.connectTimeout(Duration.ofSeconds(10))
.requestTimeout(Duration.ofSeconds(45))
.retryPolicy(D1RetryPolicy.none())
.build()) {
D1Result result = d1.query("SELECT 1 AS value");
}
```

`fromEnv()` is equivalent to `builderFromEnv().build()`. A zero connect or request timeout means no
timeout when using the default transport.

For a one-minute local check, run the bundled example:

```bash
Expand Down Expand Up @@ -131,7 +167,7 @@ Rows: 1
Pass a SQL statement as `exec.args` to try another read query:

```bash
mvn -f examples/quickstart/pom.xml exec:java -Dexec.args="SELECT 42 AS answer"
mvn -f examples/quickstart/pom.xml compile exec:java -Dexec.args="SELECT 42 AS answer"
```

Run typed mapping and opt-in write examples:
Expand Down Expand Up @@ -330,10 +366,7 @@ The default transport uses the Java standard library and does not add runtime de
```java
import java.util.Collections;

D1Client client = D1Client.builder()
.accountId(System.getenv("CLOUDFLARE_ACCOUNT_ID"))
.databaseId(System.getenv("D1_DATABASE_ID"))
.apiToken(System.getenv("CLOUDFLARE_API_TOKEN"))
D1Client client = D1Client.builderFromEnv()
.transport(request -> {
return new D1TransportResponse(200, Collections.emptyMap(), "{\"success\":true,\"result\":[]}");
})
Expand All @@ -354,6 +387,18 @@ try (D1AsyncClient d1 = D1AsyncClient.fromEnv()) {
}
```

To customize timeouts, retry behavior, mapping, transport, or the executor while retaining the
standard environment variables, start from `D1AsyncClient.builderFromEnv()`:

```java
try (D1AsyncClient d1 = D1AsyncClient.builderFromEnv()
.requestTimeout(Duration.ofSeconds(45))
.executor(executor)
.build()) {
D1Result result = d1.queryAsync("SELECT 1 AS value").join();
}
```

Async operations run on a Java `Executor` and complete futures exceptionally with the same public exception types as synchronous operations.

Run the read-only async examples:
Expand Down
9 changes: 5 additions & 4 deletions docs/guides/async.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ By default, the client creates its own executor backed by named daemon threads (
```java
ExecutorService executor = Executors.newFixedThreadPool(4);

try (D1AsyncClient d1 = D1AsyncClient.builder()
.accountId(System.getenv("CLOUDFLARE_ACCOUNT_ID"))
.databaseId(System.getenv("D1_DATABASE_ID"))
.apiToken(System.getenv("CLOUDFLARE_API_TOKEN"))
try (D1AsyncClient d1 = D1AsyncClient.builderFromEnv()
.executor(executor)
.build()) {
CompletableFuture<D1Result> future = d1.queryAsync("SELECT 1 AS value");
Expand All @@ -35,6 +32,10 @@ try (D1AsyncClient d1 = D1AsyncClient.builder()
}
```

`D1AsyncClient.fromEnv()` is equivalent to `D1AsyncClient.builderFromEnv().build()`. The
environment-backed builder also accepts timeout, retry policy, custom transport, and typed mapping
configuration before `build()`.

A caller-supplied executor is owned by your application. Closing `D1AsyncClient` never shuts it down; only the client-created default executor is shut down on close.

## Close a Client
Expand Down
7 changes: 2 additions & 5 deletions docs/guides/custom-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ D1Transport transport = request -> {
return new D1TransportResponse(200, Collections.emptyMap(), "{\"success\":true,\"result\":[]}");
};

D1Client client = D1Client.builder()
.accountId(System.getenv("CLOUDFLARE_ACCOUNT_ID"))
.databaseId(System.getenv("D1_DATABASE_ID"))
.apiToken(System.getenv("CLOUDFLARE_API_TOKEN"))
D1Client client = D1Client.builderFromEnv()
.transport(transport)
.build();
```
Expand All @@ -21,7 +18,7 @@ D1Client client = D1Client.builder()

- Return the HTTP status code, response headers, and response body exactly enough for the client to parse D1 responses.
- Preserve the `Authorization`, `Content-Type`, `Accept`, and `User-Agent` headers from the request.
- Respect `request.timeout()` where your HTTP stack supports it.
- Respect `request.timeout()` where your HTTP stack supports it. `Duration.ZERO` means no timeout.
- Do not log API tokens or authorization headers.

The custom transport does not change JSON parsing, retry behavior, or exception mapping.
Expand Down
20 changes: 19 additions & 1 deletion docs/guides/production-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,26 @@ For latency-sensitive application traffic, consider putting a Cloudflare Worker
- Do not log `Authorization` headers.
- Do not include account IDs, database IDs, tokens, customer data, or production SQL data in public issues.

The built-in `fromEnv()` helpers read:
The built-in `fromEnv()` and `builderFromEnv()` helpers read:

```text
CLOUDFLARE_ACCOUNT_ID
D1_DATABASE_ID
CLOUDFLARE_API_TOKEN
```

Use `fromEnv()` for defaults, or start from `builderFromEnv()` to retain these credential conventions
while configuring timeouts, retry behavior, a custom transport, typed mapping, or an async executor:

```java
try (D1Client d1 = D1Client.builderFromEnv()
.requestTimeout(Duration.ofSeconds(45))
.retryPolicy(D1RetryPolicy.none())
.build()) {
D1Result result = d1.query("SELECT 1 AS value");
}
```

## Retry Policy

Read queries retry transient failures by default. Write operations and batches do not retry by default because a repeated write may not be idempotent.
Expand All @@ -37,6 +49,12 @@ See [Retry Policy](retry-policy.md) for configuration examples.

Configure timeouts based on the job type. Short interactive jobs usually need lower timeouts than background maintenance jobs.

With the default transport, `connectTimeout` limits connection establishment and `requestTimeout`
is used as the `HttpURLConnection` read timeout. `Duration.ZERO` disables the corresponding
timeout. Positive sub-millisecond values are rounded up to one millisecond, and values beyond the
`HttpURLConnection` integer-millisecond range are capped. Custom transports receive the configured
request duration through `D1TransportRequest.timeout()` and are responsible for enforcing it.

Handle these exception categories separately:

- `D1AuthenticationException`: token is missing, malformed, expired, or invalid.
Expand Down
39 changes: 37 additions & 2 deletions docs/guides/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@

Java 8 or newer is required.

Gradle:
Gradle Groovy DSL:

```groovy
implementation "io.github.xxvw:cloudflare-d1-java:0.2.0"
```

Gradle Kotlin DSL:

```kotlin
implementation("io.github.xxvw:cloudflare-d1-java:0.2.0")
```

## Configure Credentials

Set the required environment variables. For local manual testing from this repository, copy the
Expand All @@ -34,6 +40,20 @@ set -a
set +a
```

PowerShell:

```powershell
Copy-Item .env.example .env
notepad .env

Get-Content .env | ForEach-Object {
if ($_ -match '^\s*([^#][^=]*)=(.*)$') {
[Environment]::SetEnvironmentVariable(
$matches[1].Trim(), $matches[2].Trim(), "Process")
}
}
```

Create the API token in the Cloudflare dashboard. For write operations, use an account-scoped D1 token with Edit permission. Store tokens outside source control and never paste production values into issues or logs.

## Run a Query
Expand All @@ -56,6 +76,21 @@ public final class Example {
}
```

When you need optional settings, initialize a builder from the same variables and customize it
before building:

```java
try (D1Client d1 = D1Client.builderFromEnv()
.connectTimeout(Duration.ofSeconds(10))
.requestTimeout(Duration.ofSeconds(45))
.retryPolicy(D1RetryPolicy.none())
.build()) {
D1Result result = d1.query("SELECT 1 AS value");
}
```

`D1Client.fromEnv()` is equivalent to `D1Client.builderFromEnv().build()`.

## Run the Repository Example

The repository includes a standalone Maven example that runs the same flow from environment variables.
Expand All @@ -79,7 +114,7 @@ Rows: 1
To run a different read query:

```bash
mvn -f examples/quickstart/pom.xml exec:java -Dexec.args="SELECT 42 AS answer"
mvn -f examples/quickstart/pom.xml compile exec:java -Dexec.args="SELECT 42 AS answer"
```

To run the read-only async examples:
Expand Down
23 changes: 22 additions & 1 deletion docs/implementation-requirements/01-public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Environment-based creation:

```java
D1Client client = D1Client.fromEnv();
D1ClientBuilder builder = D1Client.builderFromEnv();

D1AsyncClient asyncClient = D1AsyncClient.fromEnv();
D1AsyncClientBuilder asyncBuilder = D1AsyncClient.builderFromEnv();
```

Environment variables:
Expand All @@ -32,7 +36,12 @@ D1_DATABASE_ID
CLOUDFLARE_API_TOKEN
```

If an environment variable is missing or blank, `fromEnv()` must throw `IllegalStateException`.
If an environment variable is missing or blank, `fromEnv()` and `builderFromEnv()` must throw
`IllegalStateException`.

`fromEnv()` must be equivalent to `builderFromEnv().build()`. The returned builders must accept all
normal optional configuration, including timeout, retry policy, transport, mapping `ObjectMapper`,
and async executor settings.

The repository must include a tracked `.env.example` file with these variable names so contributors
can create a local ignored `.env` file for manual testing. `.env` files must remain ignored and must
Expand Down Expand Up @@ -82,6 +91,15 @@ retryPolicy = D1RetryPolicy.defaultPolicy()
userAgent = cloudflare-d1-java/{version}
```

Timeout behavior for the default transport:

- `connectTimeout` controls connection establishment.
- `requestTimeout` is applied as the `HttpURLConnection` read timeout.
- `Duration.ZERO` disables the corresponding timeout.
- Positive values below one millisecond are rounded up to one millisecond.
- Values at or above `Integer.MAX_VALUE` milliseconds are capped without overflow.
- Custom transports receive the configured request duration unchanged.

## D1Query

```java
Expand Down Expand Up @@ -175,4 +193,7 @@ Rules:

The deprecated preview async methods on `D1Client` (`queryAsync`, `executeAsync`, `batchAsync`) were removed in v0.2.0. `D1AsyncClient` is the supported asynchronous entry point.

`D1AsyncClient.builderFromEnv()` must return a fully configurable `D1AsyncClientBuilder`. A
caller-supplied executor remains caller-owned.

Async failures must complete the future exceptionally.
Loading