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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.tidesdb</groupId>
<artifactId>tidesdb-java</artifactId>
<version>0.8.1</version>
<version>0.8.2</version>
<packaging>jar</packaging>

<name>TidesDB Java</name>
Expand Down
1 change: 1 addition & 0 deletions src/main/c/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ else()
target_link_libraries(tidesdb_jni
${TIDESDB_LIBRARY}
${JNI_LIBRARIES}
${CMAKE_DL_LIBS}
)
endif()

Expand Down
99 changes: 96 additions & 3 deletions src/main/c/com_tidesdb_TidesDB.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
#include <stdlib.h>
#include <string.h>
#include <tidesdb/db.h>
#ifndef _WIN32
#include <dlfcn.h>
#endif

static void throwTidesDBException(JNIEnv *env, int errorCode, const char *message)
{
Expand Down Expand Up @@ -89,7 +92,7 @@ JNIEXPORT jlong JNICALL Java_com_tidesdb_TidesDB_nativeOpen(
jlong oscMultipartPartSize, jboolean oscSyncManifestToObject, jboolean oscReplicateWal,
jboolean oscWalUploadSync, jlong oscWalSyncThresholdBytes, jboolean oscWalSyncOnCommit,
jboolean oscReplicaMode, jlong oscReplicaSyncIntervalUs, jboolean oscReplicaReplayWal,
jint maxConcurrentFlushes, jboolean finishCompactionsOnClose)
jint maxConcurrentFlushes, jboolean finishCompactionsOnClose, jlong objStoreHandle)
{
const char *path = (*env)->GetStringUTFChars(env, dbPath, NULL);
if (path == NULL)
Expand All @@ -98,10 +101,16 @@ JNIEXPORT jlong JNICALL Java_com_tidesdb_TidesDB_nativeOpen(
return 0;
}

/* object store connector (filesystem) */
/* object store connector: a prebuilt connector handle (e.g. S3, created via
* nativeObjstoreS3Create) takes precedence; otherwise fall back to the filesystem
* connector built from objectStoreFsPath. */
tidesdb_objstore_t *obj_store = NULL;
const char *fs_path = NULL;
if (objectStoreFsPath != NULL)
if (objStoreHandle != 0)
{
obj_store = (tidesdb_objstore_t *)(uintptr_t)objStoreHandle;
}
else if (objectStoreFsPath != NULL)
{
fs_path = (*env)->GetStringUTFChars(env, objectStoreFsPath, NULL);
if (fs_path != NULL)
Expand Down Expand Up @@ -178,6 +187,90 @@ JNIEXPORT jlong JNICALL Java_com_tidesdb_TidesDB_nativeOpen(
return (jlong)(uintptr_t)db;
}

/* S3 object store support is an optional build feature of the core library
* (TIDESDB_WITH_S3=ON). Resolve the factory at runtime via dlsym so this JNI library links and
* loads against a core build that lacks S3 -- callers get a clear exception instead of a
* load-time failure. */
typedef tidesdb_objstore_t *(*tdb_s3_create_config_fn)(const tidesdb_objstore_s3_config_t *);

static tdb_s3_create_config_fn resolve_s3_create_config(void)
{
#ifdef _WIN32
return NULL; /* S3 connector is not exposed on Windows builds */
#else
return (tdb_s3_create_config_fn)dlsym(RTLD_DEFAULT, "tidesdb_objstore_s3_create_config");
#endif
}

JNIEXPORT jboolean JNICALL Java_com_tidesdb_TidesDB_nativeS3Available(JNIEnv *env, jclass cls)
{
(void)env;
(void)cls;
return resolve_s3_create_config() != NULL ? JNI_TRUE : JNI_FALSE;
}

JNIEXPORT jlong JNICALL Java_com_tidesdb_TidesDB_nativeObjstoreS3Create(
JNIEnv *env, jclass cls, jstring endpoint, jstring bucket, jstring prefix, jstring accessKey,
jstring secretKey, jstring region, jboolean useSsl, jboolean usePathStyle, jstring tlsCaPath,
jboolean tlsInsecureSkipVerify, jlong multipartThreshold, jlong multipartPartSize)
{
(void)cls;

tdb_s3_create_config_fn create_fn = resolve_s3_create_config();
if (create_fn == NULL)
{
throwTidesDBException(
env, TDB_ERR_INVALID_ARGS,
"TidesDB was built without S3 support (rebuild the core library with "
"TIDESDB_WITH_S3=ON)");
return 0;
}

/* required strings */
const char *c_endpoint = endpoint ? (*env)->GetStringUTFChars(env, endpoint, NULL) : NULL;
const char *c_bucket = bucket ? (*env)->GetStringUTFChars(env, bucket, NULL) : NULL;
const char *c_access = accessKey ? (*env)->GetStringUTFChars(env, accessKey, NULL) : NULL;
const char *c_secret = secretKey ? (*env)->GetStringUTFChars(env, secretKey, NULL) : NULL;
/* optional strings */
const char *c_prefix = prefix ? (*env)->GetStringUTFChars(env, prefix, NULL) : NULL;
const char *c_region = region ? (*env)->GetStringUTFChars(env, region, NULL) : NULL;
const char *c_ca = tlsCaPath ? (*env)->GetStringUTFChars(env, tlsCaPath, NULL) : NULL;

tidesdb_objstore_s3_config_t cfg = {
.endpoint = c_endpoint,
.bucket = c_bucket,
.prefix = c_prefix,
.access_key = c_access,
.secret_key = c_secret,
.region = c_region,
.use_ssl = useSsl ? 1 : 0,
.use_path_style = usePathStyle ? 1 : 0,
.tls_ca_path = c_ca,
.tls_insecure_skip_verify = tlsInsecureSkipVerify ? 1 : 0,
.multipart_threshold = (size_t)multipartThreshold,
.multipart_part_size = (size_t)multipartPartSize};

tidesdb_objstore_t *connector = create_fn(&cfg);

if (endpoint) (*env)->ReleaseStringUTFChars(env, endpoint, c_endpoint);
if (bucket) (*env)->ReleaseStringUTFChars(env, bucket, c_bucket);
if (accessKey) (*env)->ReleaseStringUTFChars(env, accessKey, c_access);
if (secretKey) (*env)->ReleaseStringUTFChars(env, secretKey, c_secret);
if (prefix) (*env)->ReleaseStringUTFChars(env, prefix, c_prefix);
if (region) (*env)->ReleaseStringUTFChars(env, region, c_region);
if (tlsCaPath) (*env)->ReleaseStringUTFChars(env, tlsCaPath, c_ca);

if (connector == NULL)
{
throwTidesDBException(env, TDB_ERR_IO,
"Failed to create S3 object store connector (check endpoint, "
"credentials, and bucket)");
return 0;
}

return (jlong)(uintptr_t)connector;
}

JNIEXPORT void JNICALL Java_com_tidesdb_TidesDB_nativeClose(JNIEnv *env, jclass cls, jlong handle)
{
tidesdb_t *db = (tidesdb_t *)(uintptr_t)handle;
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/com/tidesdb/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class Config {
private long unifiedMemtableSyncIntervalUs;
private String objectStoreFsPath;
private ObjectStoreConfig objectStoreConfig;
private S3Config objectStoreS3Config;
private int maxConcurrentFlushes;
private boolean finishCompactionsOnClose;

Expand All @@ -65,6 +66,7 @@ private Config(Builder builder) {
this.unifiedMemtableSyncIntervalUs = builder.unifiedMemtableSyncIntervalUs;
this.objectStoreFsPath = builder.objectStoreFsPath;
this.objectStoreConfig = builder.objectStoreConfig;
this.objectStoreS3Config = builder.objectStoreS3Config;
this.maxConcurrentFlushes = builder.maxConcurrentFlushes;
this.finishCompactionsOnClose = builder.finishCompactionsOnClose;
}
Expand Down Expand Up @@ -170,6 +172,16 @@ public ObjectStoreConfig getObjectStoreConfig() {
return objectStoreConfig;
}

/**
* Returns the S3-compatible object store connector configuration, or null if the database
* is not backed by S3.
*
* @return the S3 connector config, or null
*/
public S3Config getObjectStoreS3Config() {
return objectStoreS3Config;
}

public int getMaxConcurrentFlushes() {
return maxConcurrentFlushes;
}
Expand Down Expand Up @@ -205,6 +217,7 @@ public static class Builder {
private long unifiedMemtableSyncIntervalUs = 0;
private String objectStoreFsPath = null;
private ObjectStoreConfig objectStoreConfig = null;
private S3Config objectStoreS3Config = null;
private int maxConcurrentFlushes = 0;
private boolean finishCompactionsOnClose = false;

Expand Down Expand Up @@ -293,6 +306,20 @@ public Builder objectStoreConfig(ObjectStoreConfig objectStoreConfig) {
return this;
}

/**
* Backs the database with an S3-compatible object store connector (AWS S3, MinIO, etc.).
* Takes precedence over {@link #objectStoreFsPath(String)} when both are set. Pair with
* {@link #objectStoreConfig(ObjectStoreConfig)} to tune cache, multipart, and replication
* behavior. Requires the native library to be built with {@code TIDESDB_WITH_S3=ON}.
*
* @param objectStoreS3Config the S3 connector configuration, or null for none
* @return this builder
*/
public Builder objectStoreS3Config(S3Config objectStoreS3Config) {
this.objectStoreS3Config = objectStoreS3Config;
return this;
}

public Builder maxConcurrentFlushes(int maxConcurrentFlushes) {
this.maxConcurrentFlushes = maxConcurrentFlushes;
return this;
Expand Down
163 changes: 163 additions & 0 deletions src/main/java/com/tidesdb/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
*
* Copyright (C) TidesDB
*
* Original Author: Alex Gaetano Padula
*
* Licensed under the Mozilla Public License, v. 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.mozilla.org/en-US/MPL/2.0/
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tidesdb;

/**
* Configuration for an S3-compatible object store connector (AWS S3, MinIO, etc.).
*
* <p>Mirrors {@code tidesdb_objstore_s3_config_t}. Set it on a {@link Config} via
* {@link Config.Builder#objectStoreS3Config(S3Config)} to back a database with object storage.
* Combine it with an {@link ObjectStoreConfig} (cache, multipart, WAL replication, replica mode)
* for full control over object store behavior.
*
* <p>Requires the native TidesDB library to have been built with {@code TIDESDB_WITH_S3=ON};
* otherwise opening the database throws a {@link TidesDBException}. Use
* {@link TidesDB#isS3Available()} to probe support at runtime.
*/
public class S3Config {

private final String endpoint;
private final String bucket;
private final String prefix;
private final String accessKey;
private final String secretKey;
private final String region;
private final boolean useSsl;
private final boolean usePathStyle;
private final String tlsCaPath;
private final boolean tlsInsecureSkipVerify;
private final long multipartThreshold;
private final long multipartPartSize;

private S3Config(Builder builder) {
this.endpoint = builder.endpoint;
this.bucket = builder.bucket;
this.prefix = builder.prefix;
this.accessKey = builder.accessKey;
this.secretKey = builder.secretKey;
this.region = builder.region;
this.useSsl = builder.useSsl;
this.usePathStyle = builder.usePathStyle;
this.tlsCaPath = builder.tlsCaPath;
this.tlsInsecureSkipVerify = builder.tlsInsecureSkipVerify;
this.multipartThreshold = builder.multipartThreshold;
this.multipartPartSize = builder.multipartPartSize;
}

public static Builder builder() {
return new Builder();
}

public String getEndpoint() { return endpoint; }
public String getBucket() { return bucket; }
public String getPrefix() { return prefix; }
public String getAccessKey() { return accessKey; }
public String getSecretKey() { return secretKey; }
public String getRegion() { return region; }
public boolean isUseSsl() { return useSsl; }
public boolean isUsePathStyle() { return usePathStyle; }
public String getTlsCaPath() { return tlsCaPath; }
public boolean isTlsInsecureSkipVerify() { return tlsInsecureSkipVerify; }
public long getMultipartThreshold() { return multipartThreshold; }
public long getMultipartPartSize() { return multipartPartSize; }

/**
* Builder for {@link S3Config}. {@code endpoint}, {@code bucket}, {@code accessKey}, and
* {@code secretKey} are required; the rest default to secure, AWS-friendly values
* (HTTPS on, virtual-hosted URLs, TLS verification enabled, built-in multipart sizing).
*/
public static class Builder {
private String endpoint;
private String bucket;
private String prefix = null;
private String accessKey;
private String secretKey;
private String region = null;
private boolean useSsl = true;
private boolean usePathStyle = false;
private String tlsCaPath = null;
private boolean tlsInsecureSkipVerify = false;
private long multipartThreshold = 0; // 0 = library default
private long multipartPartSize = 0; // 0 = library default

/** S3 endpoint, e.g. "s3.amazonaws.com" or "minio.local:9000" (required). */
public Builder endpoint(String endpoint) { this.endpoint = endpoint; return this; }

/** Bucket name (required). */
public Builder bucket(String bucket) { this.bucket = bucket; return this; }

/** Key prefix, e.g. "production/db1/" (optional). */
public Builder prefix(String prefix) { this.prefix = prefix; return this; }

/** AWS access key ID (required). */
public Builder accessKey(String accessKey) { this.accessKey = accessKey; return this; }

/** AWS secret access key (required). */
public Builder secretKey(String secretKey) { this.secretKey = secretKey; return this; }

/** AWS region, e.g. "us-east-1"; null for MinIO/default. */
public Builder region(String region) { this.region = region; return this; }

/** 1 for HTTPS (default), 0 for HTTP. */
public Builder useSsl(boolean useSsl) { this.useSsl = useSsl; return this; }

/** Path-style URLs (MinIO) when true; virtual-hosted (AWS) when false (default). */
public Builder usePathStyle(boolean usePathStyle) { this.usePathStyle = usePathStyle; return this; }

/** Custom CA bundle file path, or null for the system bundle. */
public Builder tlsCaPath(String tlsCaPath) { this.tlsCaPath = tlsCaPath; return this; }

/**
* Disable TLS peer and host verification when true (test only, insecure). Default false
* keeps verification on.
*/
public Builder tlsInsecureSkipVerify(boolean tlsInsecureSkipVerify) {
this.tlsInsecureSkipVerify = tlsInsecureSkipVerify;
return this;
}

/** Object size at/above which multipart upload is used; 0 uses the library default. */
public Builder multipartThreshold(long multipartThreshold) {
this.multipartThreshold = multipartThreshold;
return this;
}

/** Multipart chunk size in bytes; 0 uses the library default. */
public Builder multipartPartSize(long multipartPartSize) {
this.multipartPartSize = multipartPartSize;
return this;
}

public S3Config build() {
if (endpoint == null || endpoint.isEmpty()) {
throw new IllegalArgumentException("S3 endpoint is required");
}
if (bucket == null || bucket.isEmpty()) {
throw new IllegalArgumentException("S3 bucket is required");
}
if (accessKey == null || accessKey.isEmpty()) {
throw new IllegalArgumentException("S3 access key is required");
}
if (secretKey == null || secretKey.isEmpty()) {
throw new IllegalArgumentException("S3 secret key is required");
}
return new S3Config(this);
}
}
}
Loading
Loading