diff --git a/README.md b/README.md index 96019721b..c14100f15 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,79 @@ yourself. } ``` +## Migrate from the NebulaGraph v3 Java client + +`client-v3compat` is a source-compatible re-implementation of the NebulaGraph v3 Java client +(`com.vesoft.nebula.client.*`) under the `com.vesoft.nebula.driver.v3client` namespace. It keeps the +v3 API surface (`NebulaPool` / `Session` / `SessionPool` / `ResultSet` / `ValueWrapper` / `Node` / +`Relationship` / `PathWrapper`, …) and delegates to the v5 `driver` internally, so an existing v3 +application can be migrated with minimal changes. + +### Steps + +1. Replace the Maven dependency: + +```xml + + + com.vesoft + client + 3.x.x + + + + + com.vesoft + client-v3compat + 5.3-SNAPSHOT + +``` + +2. Rewrite the imports: `com.vesoft.nebula.client.` → `com.vesoft.nebula.driver.v3client.` + (and `com.vesoft.nebula.ErrorCode` → `com.vesoft.nebula.driver.v3client.graph.ErrorCode`). + +3. Migrate the GQL statements from nGQL to ISO-GQL. This is **not** handled by the compatibility + layer — e.g. `USE space` → `USE graph`, `INSERT VERTEX/EDGE` → `INSERT OR IGNORE`, `GO/FETCH/ + LOOKUP` → `MATCH`, and the old `MATCH` syntax → v5 `MATCH`. + +### Example + +```java +import com.vesoft.nebula.driver.v3client.graph.NebulaPoolConfig; +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import com.vesoft.nebula.driver.v3client.graph.net.NebulaPool; +import com.vesoft.nebula.driver.v3client.graph.net.Session; +import java.util.Arrays; + +NebulaPool pool = new NebulaPool(); +NebulaPoolConfig config = new NebulaPoolConfig(); +config.setMaxConnSize(10); +pool.init(Arrays.asList(new HostAddress("127.0.0.1", 9669)), config); + +Session session = pool.getSession("root", "nebula", false); +ResultSet rs = session.execute("MATCH (v:player) RETURN v LIMIT 1"); // ISO-GQL +if (rs.isSucceeded()) { + System.out.println(rs.rowValues(0).get("v")); +} +session.release(); +pool.close(); +``` + +A `SessionPool` variant is shown in +`examples/src/main/java/com/vesoft/nebula/V3SessionPoolExample.java`. + +### Scope and declared differences + +- Only the v3 `graph` package is covered; `meta` / `storage` / `encoder` have no v5 equivalents. +- `Node.getId()` and `Relationship.srcId()/dstId()` return the v5 numeric id as a `ValueWrapper`; + string ids are no longer recoverable in v5. +- Thrift-coupled methods (`ResultSet.getRows()`, `getPlanDesc()`, `ValueWrapper.getValue()`) are + adapted or removed; v5 `DECIMAL` values are exposed via `ValueWrapper.isDouble()/asDouble()`. + +See `migration_guide_v3.md` for the full migration guide, including GQL rewrites and the +complete list of known differences. + ## Note If your packaged jar project that imports the NebulaGraph client dependency happens diff --git a/client-v3compat/pom.xml b/client-v3compat/pom.xml new file mode 100644 index 000000000..500a8eca0 --- /dev/null +++ b/client-v3compat/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + com.vesoft + nebula + 5.3-SNAPSHOT + + + client-v3compat + NebulaGraph Java Driver v3 compatibility layer + + Source-compatible re-implementation of the NebulaGraph v3 Java client (com.vesoft.nebula.client.graph) + under the com.vesoft.nebula.driver.v3client namespace, delegating to the v5 driver. + + + + 8 + 8 + UTF-8 + + + + + com.vesoft + driver + ${project.version} + + + org.apache.commons + commons-pool2 + 2.2 + + + org.slf4j + slf4j-api + 1.7.25 + + + com.alibaba + fastjson + 1.2.83 + + + junit + junit + 4.13.1 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/ErrorCode.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/ErrorCode.java new file mode 100644 index 000000000..720800d6c --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/ErrorCode.java @@ -0,0 +1,263 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph; + +/** + * v3-style integer error codes. + * + *

This enum mirrors the v3 client's {@code com.vesoft.nebula.ErrorCode} so that v3 + * applications can keep using checks like {@code resultSet.getErrorCode() == + * ErrorCode.E_SEMANTIC_ERROR.getValue()} after switching to the compatibility namespace. + */ +public enum ErrorCode { + SUCCEEDED(0), + E_DISCONNECTED(-1), + E_FAIL_TO_CONNECT(-2), + E_RPC_FAILURE(-3), + E_LEADER_CHANGED(-4), + E_SPACE_NOT_FOUND(-5), + E_TAG_NOT_FOUND(-6), + E_EDGE_NOT_FOUND(-7), + E_INDEX_NOT_FOUND(-8), + E_EDGE_PROP_NOT_FOUND(-9), + E_TAG_PROP_NOT_FOUND(-10), + E_ROLE_NOT_FOUND(-11), + E_CONFIG_NOT_FOUND(-12), + E_MACHINE_NOT_FOUND(-13), + E_ZONE_NOT_FOUND(-14), + E_LISTENER_NOT_FOUND(-15), + E_PART_NOT_FOUND(-16), + E_KEY_NOT_FOUND(-17), + E_USER_NOT_FOUND(-18), + E_STATS_NOT_FOUND(-19), + E_SERVICE_NOT_FOUND(-20), + E_DRAINER_NOT_FOUND(-21), + E_DRAINER_CLIENT_NOT_FOUND(-22), + E_PART_STOPPED(-23), + E_BACKUP_FAILED(-24), + E_BACKUP_EMPTY_TABLE(-25), + E_BACKUP_TABLE_FAILED(-26), + E_PARTIAL_RESULT(-27), + E_REBUILD_INDEX_FAILED(-28), + E_INVALID_PASSWORD(-29), + E_FAILED_GET_ABS_PATH(-30), + E_LISTENER_PROGRESS_FAILED(-31), + E_SYNC_LISTENER_NOT_FOUND(-32), + E_DRAINER_PROGRESS_FAILED(-33), + E_PART_DISABLED(-34), + E_PART_ALREADY_STARTED(-35), + E_PART_ALREADY_STOPPED(-36), + E_QUERY_TIMEDOUT(-37), + E_BAD_USERNAME_PASSWORD(-1001), + E_SESSION_INVALID(-1002), + E_SESSION_TIMEOUT(-1003), + E_SYNTAX_ERROR(-1004), + E_EXECUTION_ERROR(-1005), + E_STATEMENT_EMPTY(-1006), + E_BAD_PERMISSION(-1008), + E_SEMANTIC_ERROR(-1009), + E_TOO_MANY_CONNECTIONS(-1010), + E_PARTIAL_SUCCEEDED(-1011), + E_NO_HOSTS(-2001), + E_EXISTED(-2002), + E_INVALID_HOST(-2003), + E_UNSUPPORTED(-2004), + E_NOT_DROP(-2005), + E_BALANCER_RUNNING(-2006), + E_CONFIG_IMMUTABLE(-2007), + E_CONFLICT(-2008), + E_INVALID_PARM(-2009), + E_WRONGCLUSTER(-2010), + E_ZONE_NOT_ENOUGH(-2011), + E_ZONE_IS_EMPTY(-2012), + E_LISTENER_CONFLICT(-2013), + E_SCHEMA_NAME_EXISTS(-2014), + E_RELATED_INDEX_EXISTS(-2015), + E_RELATED_SPACE_EXISTS(-2016), + E_RELATED_FULLTEXT_INDEX_EXISTS(-2017), + E_HISTORY_CONFLICT(-2018), + E_ZONE_IS_ENABLED(-2019), + E_STORE_FAILURE(-2021), + E_STORE_SEGMENT_ILLEGAL(-2022), + E_BAD_BALANCE_PLAN(-2023), + E_BALANCED(-2024), + E_NO_RUNNING_BALANCE_PLAN(-2025), + E_NO_VALID_HOST(-2026), + E_CORRUPTED_BALANCE_PLAN(-2027), + E_NO_INVALID_BALANCE_PLAN(-2028), + E_NO_VALID_DRAINER(-2029), + E_IMPROPER_ROLE(-2030), + E_INVALID_PARTITION_NUM(-2031), + E_INVALID_REPLICA_FACTOR(-2032), + E_INVALID_CHARSET(-2033), + E_INVALID_COLLATE(-2034), + E_CHARSET_COLLATE_NOT_MATCH(-2035), + E_PRIVILEGE_ALL_TAG_EDGE_SETTLED(-2036), + E_PRIVILEGE_NOT_EXIST(-2037), + E_PRIVILEGE_NEED_BASIC_ROLE(-2038), + E_PRIVILEGE_ACTION_INVALID(-2039), + E_STORAGE_ENABLE_AUTH(-2058), + E_SNAPSHOT_FAILURE(-2040), + E_SNAPSHOT_RUNNING_JOBS(-2056), + E_SNAPSHOT_NOT_FOUND(-2057), + E_BLOCK_WRITE_FAILURE(-2041), + E_REBUILD_INDEX_FAILURE(-2042), + E_INDEX_WITH_TTL(-2043), + E_ADD_JOB_FAILURE(-2044), + E_STOP_JOB_FAILURE(-2045), + E_SAVE_JOB_FAILURE(-2046), + E_BALANCER_FAILURE(-2047), + E_JOB_NOT_FINISHED(-2048), + E_TASK_REPORT_OUT_DATE(-2049), + E_JOB_NOT_IN_SPACE(-2050), + E_JOB_NEED_RECOVER(-2051), + E_JOB_ALREADY_FINISH(-2052), + E_JOB_SUBMITTED(-2053), + E_JOB_NOT_STOPPABLE(-2054), + E_JOB_HAS_NO_TARGET_STORAGE(-2055), + E_INVALID_JOB(-2065), + E_BACKUP_RUNNING_JOBS(-2066), + E_BACKUP_SPACE_NOT_FOUND(-2067), + E_RESTORE_FAILURE(-2068), + E_SESSION_NOT_FOUND(-2069), + E_LIST_CLUSTER_FAILURE(-2070), + E_LIST_CLUSTER_GET_ABS_PATH_FAILURE(-2071), + E_LIST_CLUSTER_NO_AGENT_FAILURE(-2072), + E_QUERY_NOT_FOUND(-2073), + E_AGENT_HB_FAILUE(-2074), + E_INVALID_VARIABLE(-2080), + E_VARIABLE_TYPE_VALUE_MISMATCH(-2081), + E_HOST_CAN_NOT_BE_ADDED(-2082), + E_ACCESS_ES_FAILURE(-2090), + E_GRAPH_MEMORY_EXCEEDED(-2600), + E_CONSENSUS_ERROR(-3001), + E_KEY_HAS_EXISTS(-3002), + E_DATA_TYPE_MISMATCH(-3003), + E_INVALID_FIELD_VALUE(-3004), + E_INVALID_OPERATION(-3005), + E_NOT_NULLABLE(-3006), + E_FIELD_UNSET(-3007), + E_OUT_OF_RANGE(-3008), + E_DATA_CONFLICT_ERROR(-3010), + E_WRITE_STALLED(-3011), + E_IMPROPER_DATA_TYPE(-3021), + E_INVALID_SPACEVIDLEN(-3022), + E_INVALID_FILTER(-3031), + E_INVALID_UPDATER(-3032), + E_INVALID_STORE(-3033), + E_INVALID_PEER(-3034), + E_RETRY_EXHAUSTED(-3035), + E_TRANSFER_LEADER_FAILED(-3036), + E_INVALID_STAT_TYPE(-3037), + E_INVALID_VID(-3038), + E_NO_TRANSFORMED(-3039), + E_LOAD_META_FAILED(-3040), + E_FAILED_TO_CHECKPOINT(-3041), + E_CHECKPOINT_BLOCKED(-3042), + E_FILTER_OUT(-3043), + E_INVALID_DATA(-3044), + E_MUTATE_EDGE_CONFLICT(-3045), + E_MUTATE_TAG_CONFLICT(-3046), + E_OUTDATED_LOCK(-3047), + E_INVALID_TASK_PARA(-3051), + E_USER_CANCEL(-3052), + E_TASK_EXECUTION_FAILED(-3053), + E_PLAN_IS_KILLED(-3060), + E_NO_TERM(-3070), + E_OUTDATED_TERM(-3071), + E_OUTDATED_EDGE(-3072), + E_WRITE_WRITE_CONFLICT(-3073), + E_CLIENT_SERVER_INCOMPATIBLE(-3061), + E_ID_FAILED(-3062), + E_RAFT_UNKNOWN_PART(-3500), + E_RAFT_LOG_GAP(-3501), + E_RAFT_LOG_STALE(-3502), + E_RAFT_TERM_OUT_OF_DATE(-3503), + E_RAFT_UNKNOWN_APPEND_LOG(-3504), + E_RAFT_WAITING_SNAPSHOT(-3511), + E_RAFT_SENDING_SNAPSHOT(-3512), + E_RAFT_INVALID_PEER(-3513), + E_RAFT_NOT_READY(-3514), + E_RAFT_STOPPED(-3515), + E_RAFT_BAD_ROLE(-3516), + E_RAFT_WAL_FAIL(-3521), + E_RAFT_HOST_STOPPED(-3522), + E_RAFT_TOO_MANY_REQUESTS(-3523), + E_RAFT_PERSIST_SNAPSHOT_FAILED(-3524), + E_RAFT_RPC_EXCEPTION(-3525), + E_RAFT_NO_WAL_FOUND(-3526), + E_RAFT_HOST_PAUSED(-3527), + E_RAFT_WRITE_BLOCKED(-3528), + E_RAFT_BUFFER_OVERFLOW(-3529), + E_RAFT_ATOMIC_OP_FAILED(-3530), + E_LEADER_LEASE_FAILED(-3531), + E_RAFT_CAUGHT_UP(-3532), + E_LOG_GAP(-4001), + E_LOG_STALE(-4002), + E_INVALID_DRAINER_STORE(-4003), + E_SPACE_MISMATCH(-4004), + E_PART_MISMATCH(-4005), + E_DATA_CONFLICT(-4006), + E_REQ_CONFLICT(-4007), + E_DATA_ILLEGAL(-4008), + E_CACHE_CONFIG_ERROR(-5001), + E_NOT_ENOUGH_SPACE(-5002), + E_CACHE_MISS(-5003), + E_POOL_NOT_FOUND(-5004), + E_CACHE_WRITE_FAILURE(-5005), + E_NODE_NUMBER_EXCEED_LIMIT(-7001), + E_TOTAL_CPU_CORE_EXCEED_LIMIT(-7002), + E_INVALID_LICENSE_MANAGER_STATUS(-7003), + E_STORAGE_MEMORY_EXCEEDED(-3600), + E_UNKNOWN(-8000); + + private final int value; + + ErrorCode(int value) { + this.value = value; + } + + /** + * @return the integer value of this error code, matching the v3 client. + */ + public int getValue() { + return value; + } + + /** + * Map a v5 driver {@link com.vesoft.nebula.driver.graph.ErrorCode} to a v3-style integer + * error code. The mapping is best-effort: syntax/semantic/timeout codes are translated to + * their v3 equivalents, everything else falls back to {@link #E_EXECUTION_ERROR}. + * + * @param code the v5 error code + * @return the corresponding v3-style integer error code + */ + public static int fromV5ErrorCode(com.vesoft.nebula.driver.graph.ErrorCode code) { + if (code == null) { + return E_UNKNOWN.getValue(); + } + switch (code) { + case SUCCESSFUL_COMPLETION: + return SUCCEEDED.getValue(); + case SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION: + case INVALID_SYNTAX: + return E_SYNTAX_ERROR.getValue(); + case DATA_EXCEPTION: + case INVALID_VALUE_TYPE: + case VALUES_NOT_COMPARABLE: + return E_SEMANTIC_ERROR.getValue(); + default: + String c = code.code; + if (c != null && c.startsWith("42")) { + return E_SYNTAX_ERROR.getValue(); + } + if (c != null && c.startsWith("22")) { + return E_SEMANTIC_ERROR.getValue(); + } + return E_EXECUTION_ERROR.getValue(); + } + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/NebulaPoolConfig.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/NebulaPoolConfig.java new file mode 100644 index 000000000..a89a2c39e --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/NebulaPoolConfig.java @@ -0,0 +1,131 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph; + +import com.vesoft.nebula.driver.v3client.graph.data.SSLParam; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * Connection-pool configuration, matching the v3 client. + * + *

{@code useHttp2} and {@code customHeaders} have no equivalent in the v5 gRPC driver and are + * kept for source compatibility only (they are ignored at runtime). + */ +public class NebulaPoolConfig implements Serializable { + + private static final long serialVersionUID = 3977910115039279651L; + + private int minConnsSize = 0; + private int maxConnsSize = 10; + private int timeout = 0; + private int idleTime = 0; + private int intervalIdle = -1; + private int waitTime = 0; + private double minClusterHealthRate = 1; + private boolean enableSsl = false; + private SSLParam sslParam = null; + private boolean useHttp2 = false; + private Map customHeaders = new HashMap<>(); + + public boolean isEnableSsl() { + return enableSsl; + } + + public void setEnableSsl(boolean enableSsl) { + this.enableSsl = enableSsl; + } + + public SSLParam getSslParam() { + return sslParam; + } + + public void setSslParam(SSLParam sslParam) { + this.sslParam = sslParam; + } + + public int getMinConnSize() { + return minConnsSize; + } + + public NebulaPoolConfig setMinConnSize(int minConnSize) { + this.minConnsSize = minConnSize; + return this; + } + + public int getMaxConnSize() { + return maxConnsSize; + } + + public NebulaPoolConfig setMaxConnSize(int maxConnSize) { + this.maxConnsSize = maxConnSize; + return this; + } + + public int getTimeout() { + return timeout; + } + + public NebulaPoolConfig setTimeout(int timeout) { + this.timeout = timeout; + return this; + } + + public int getIdleTime() { + return idleTime; + } + + public NebulaPoolConfig setIdleTime(int idleTime) { + this.idleTime = idleTime; + return this; + } + + public int getIntervalIdle() { + return intervalIdle; + } + + public NebulaPoolConfig setIntervalIdle(int intervalIdle) { + this.intervalIdle = intervalIdle; + return this; + } + + public int getWaitTime() { + return waitTime; + } + + public NebulaPoolConfig setWaitTime(int waitTime) { + this.waitTime = waitTime; + return this; + } + + public double getMinClusterHealthRate() { + return minClusterHealthRate; + } + + public NebulaPoolConfig setMinClusterHealthRate(double minClusterHealthRate) { + this.minClusterHealthRate = minClusterHealthRate; + return this; + } + + public boolean isUseHttp2() { + return useHttp2; + } + + public NebulaPoolConfig setUseHttp2(boolean useHttp2) { + this.useHttp2 = useHttp2; + return this; + } + + public Map getCustomHeaders() { + return customHeaders; + } + + public NebulaPoolConfig setCustomHeaders(Map customHeaders) { + this.customHeaders = customHeaders; + return this; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/NebulaSession.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/NebulaSession.java new file mode 100644 index 000000000..cb0e17943 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/NebulaSession.java @@ -0,0 +1,119 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph; + +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import com.vesoft.nebula.driver.v3client.graph.exception.IOErrorException; +import com.vesoft.nebula.driver.v3client.graph.net.Session; +import com.vesoft.nebula.driver.v3client.graph.net.SessionState; +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A pooled session, matching the v3 client's {@code NebulaSession}. + * + *

Each instance wraps one v5 driver {@link com.vesoft.nebula.driver.graph.net.NebulaClient} + * (one server-side session). + */ +public class NebulaSession implements Serializable { + + private static final long serialVersionUID = -88438249377120255L; + + private final long sessionID; + private final int timezoneOffset; + + private volatile com.vesoft.nebula.driver.graph.net.NebulaClient client; + private final AtomicReference state = new AtomicReference<>(); + private final AtomicBoolean isReleased = new AtomicBoolean(false); + + public NebulaSession(com.vesoft.nebula.driver.graph.net.NebulaClient client, + SessionState state) { + this.client = client; + this.sessionID = client.getSessionId(); + this.timezoneOffset = 0; + this.state.set(state); + } + + public long getSessionID() { + return sessionID; + } + + public Boolean isIdle() { + return state.get() == SessionState.IDLE; + } + + public Boolean isUsed() { + return state.get() == SessionState.USED; + } + + public boolean isUsedAndSetIdle() { + return state.compareAndSet(SessionState.USED, SessionState.IDLE); + } + + public boolean isIdleAndSetUsed() { + return state.compareAndSet(SessionState.IDLE, SessionState.USED); + } + + public ResultSet execute(String stmt) throws IOErrorException { + return executeWithParameter(stmt, Collections.emptyMap()); + } + + public ResultSet executeWithParameter(String stmt, Map parameterMap) + throws IOErrorException { + checkReleased(); + String gql = Session.inlineParameters(stmt, parameterMap); + try { + return new ResultSet(client.execute(gql), timezoneOffset); + } catch (com.vesoft.nebula.driver.graph.exception.IOErrorException e) { + throw Session.toCompat(e); + } + } + + public ResultSet executeWithTimeout(String stmt, long timeoutMs) throws IOErrorException { + return executeWithParameterTimeout(stmt, Collections.emptyMap(), timeoutMs); + } + + public ResultSet executeWithParameterTimeout(String stmt, + Map parameterMap, + long timeoutMs) throws IOErrorException { + checkReleased(); + if (timeoutMs <= 0) { + throw new IllegalArgumentException("timeout should be a positive number"); + } + String gql = Session.inlineParameters(stmt, parameterMap); + try { + return new ResultSet(client.execute(gql, timeoutMs), timezoneOffset); + } catch (com.vesoft.nebula.driver.graph.exception.IOErrorException e) { + throw Session.toCompat(e); + } + } + + public String executeJsonWithParameter(String stmt, Map parameterMap) + throws IOErrorException { + return Session.toJson(executeWithParameter(stmt, parameterMap)); + } + + public void release() { + if (isReleased.compareAndSet(false, true)) { + try { + client.close(); + } catch (Exception e) { + // ignore; the connection is being released anyway. + } + client = null; + } + } + + private void checkReleased() throws IOErrorException { + if (client == null) { + throw new IOErrorException(IOErrorException.E_CONNECT_BROKEN, + "The session was released, couldn't use again."); + } + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/SessionPool.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/SessionPool.java new file mode 100644 index 000000000..c02a44f8d --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/SessionPool.java @@ -0,0 +1,484 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph; + +import com.alibaba.fastjson.JSON; +import com.vesoft.nebula.driver.v3client.graph.data.CASignedSSLParam; +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import com.vesoft.nebula.driver.v3client.graph.data.SSLParam; +import com.vesoft.nebula.driver.v3client.graph.data.SelfSignedSSLParam; +import com.vesoft.nebula.driver.v3client.graph.exception.AuthFailedException; +import com.vesoft.nebula.driver.v3client.graph.exception.BindSpaceFailedException; +import com.vesoft.nebula.driver.v3client.graph.exception.ClientServerIncompatibleException; +import com.vesoft.nebula.driver.v3client.graph.exception.IOErrorException; +import com.vesoft.nebula.driver.v3client.graph.net.Session; +import com.vesoft.nebula.driver.v3client.graph.net.SessionState; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A pool of sessions, matching the v3 client's {@code SessionPool}. + */ +public class SessionPool implements Serializable { + + private static final long serialVersionUID = 6051248334277617891L; + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final ScheduledExecutorService healthCheckSchedule = + Executors.newScheduledThreadPool(1); + private final ScheduledExecutorService sessionQueueMaintainSchedule = + Executors.newScheduledThreadPool(1); + + public CopyOnWriteArrayList sessionList = new CopyOnWriteArrayList<>(); + public AtomicInteger idleSessionSize = new AtomicInteger(0); + public AtomicBoolean hasInit = new AtomicBoolean(false); + public AtomicBoolean isClosed = new AtomicBoolean(false); + + private final AtomicInteger pos = new AtomicInteger(0); + + private final SessionPoolConfig sessionPoolConfig; + private final int minSessionSize; + private final int maxSessionSize; + private final int cleanTime; + private final int healthCheckTime; + private final int retryTimes; + private final int intervalTime; + private final boolean reconnect; + private final String spaceName; + + public SessionPool(SessionPoolConfig poolConfig) { + this.sessionPoolConfig = poolConfig; + this.minSessionSize = poolConfig.getMinSessionSize(); + this.maxSessionSize = poolConfig.getMaxSessionSize(); + this.cleanTime = poolConfig.getCleanTime(); + this.retryTimes = poolConfig.getRetryTimes(); + this.intervalTime = poolConfig.getIntervalTime(); + this.reconnect = poolConfig.isReconnect(); + this.healthCheckTime = poolConfig.getHealthCheckTime(); + this.spaceName = poolConfig.getSpaceName(); + init(); + } + + private synchronized NebulaSession getSession() + throws ClientServerIncompatibleException, AuthFailedException, IOErrorException, + BindSpaceFailedException { + int retry = sessionPoolConfig.getRetryConnectTimes(); + while (retry-- >= 0) { + if (idleSessionSize.get() > 0) { + for (NebulaSession nebulaSession : sessionList) { + if (nebulaSession.isIdleAndSetUsed()) { + idleSessionSize.decrementAndGet(); + return nebulaSession; + } + } + } + if (sessionList.size() < maxSessionSize) { + return createSessionObject(SessionState.USED); + } + try { + Thread.sleep(sessionPoolConfig.getWaitTime()); + } catch (InterruptedException e) { + log.error("getSession error when wait for idle sessions, ", e); + throw new RuntimeException(e); + } + } + throw new RuntimeException("no extra session available"); + } + + @Deprecated + public boolean init() { + if (hasInit.get()) { + return true; + } + while (sessionList.size() < minSessionSize) { + try { + createSessionObject(SessionState.IDLE); + idleSessionSize.incrementAndGet(); + } catch (Exception e) { + log.error("SessionPool init failed. ", e); + throw new RuntimeException("create session failed.", e); + } + } + healthCheckSchedule.scheduleAtFixedRate(this::checkSession, 0, healthCheckTime, + TimeUnit.SECONDS); + sessionQueueMaintainSchedule.scheduleAtFixedRate(this::updateSessionQueue, 0, cleanTime, + TimeUnit.SECONDS); + hasInit.compareAndSet(false, true); + return true; + } + + public ResultSet execute(String stmt) throws IOErrorException, + ClientServerIncompatibleException, AuthFailedException, BindSpaceFailedException { + return execute(stmt, Collections.emptyMap()); + } + + public ResultSet execute(String stmt, Map parameterMap) + throws ClientServerIncompatibleException, AuthFailedException, + IOErrorException, BindSpaceFailedException { + stmtCheck(stmt); + checkSessionPool(); + NebulaSession nebulaSession = null; + ResultSet resultSet = null; + int tryTimes = 0; + while (tryTimes++ <= retryTimes) { + try { + nebulaSession = getSession(); + resultSet = nebulaSession.executeWithParameter(stmt, parameterMap); + if (resultSet.isSucceeded() + || resultSet.getErrorCode() == ErrorCode.E_SEMANTIC_ERROR.getValue() + || resultSet.getErrorCode() == ErrorCode.E_SYNTAX_ERROR.getValue() + || resultSet.getErrorCode() == ErrorCode.E_QUERY_TIMEDOUT.getValue()) { + releaseSession(nebulaSession); + return resultSet; + } + log.warn(String.format("execute error, code: %d, message: %s, retry: %d", + resultSet.getErrorCode(), resultSet.getErrorMessage(), + tryTimes)); + nebulaSession.release(); + sessionList.remove(nebulaSession); + try { + Thread.sleep(intervalTime); + } catch (InterruptedException ignored) { + // ignore + } + } catch (ClientServerIncompatibleException e) { + // will never get here. + } catch (AuthFailedException | BindSpaceFailedException e) { + throw e; + } catch (IOErrorException e) { + if (nebulaSession != null) { + nebulaSession.release(); + sessionList.remove(nebulaSession); + } + if (tryTimes < retryTimes) { + log.warn(String.format("execute failed for IOErrorException, message: %s, " + + "retry: %d", e.getMessage(), tryTimes)); + try { + Thread.sleep(intervalTime); + } catch (InterruptedException ignored) { + // ignore + } + } else { + throw e; + } + } + } + if (nebulaSession != null) { + nebulaSession.release(); + sessionList.remove(nebulaSession); + } + return resultSet; + } + + public ResultSet executeWithTimeout(String stmt, long timeoutMs) + throws IOErrorException, AuthFailedException, BindSpaceFailedException { + return executeWithParameterTimeout(stmt, Collections.emptyMap(), timeoutMs); + } + + public ResultSet executeWithParameterTimeout(String stmt, + Map parameterMap, + long timeoutMs) + throws IOErrorException, AuthFailedException, BindSpaceFailedException { + if (timeoutMs <= 0) { + throw new IllegalArgumentException("timeout should be a positive number"); + } + stmtCheck(stmt); + checkSessionPool(); + NebulaSession nebulaSession = null; + ResultSet resultSet = null; + int tryTimes = 0; + while (tryTimes++ <= retryTimes) { + try { + nebulaSession = getSession(); + resultSet = nebulaSession.executeWithParameterTimeout(stmt, parameterMap, timeoutMs); + if (resultSet.isSucceeded() + || resultSet.getErrorCode() == ErrorCode.E_SEMANTIC_ERROR.getValue() + || resultSet.getErrorCode() == ErrorCode.E_SYNTAX_ERROR.getValue() + || resultSet.getErrorCode() == ErrorCode.E_QUERY_TIMEDOUT.getValue()) { + releaseSession(nebulaSession); + return resultSet; + } + log.warn(String.format("execute error, code: %d, message: %s, retry: %d", + resultSet.getErrorCode(), resultSet.getErrorMessage(), + tryTimes)); + nebulaSession.release(); + sessionList.remove(nebulaSession); + try { + Thread.sleep(intervalTime); + } catch (InterruptedException ignored) { + // ignore + } + } catch (ClientServerIncompatibleException e) { + // will never get here. + } catch (AuthFailedException | BindSpaceFailedException e) { + throw e; + } catch (IOErrorException e) { + if (nebulaSession != null) { + nebulaSession.release(); + sessionList.remove(nebulaSession); + } + if (tryTimes < retryTimes) { + log.warn(String.format("execute failed for IOErrorException, message: %s, " + + "retry: %d", e.getMessage(), tryTimes)); + try { + Thread.sleep(intervalTime); + } catch (InterruptedException ignored) { + // ignore + } + } else { + throw e; + } + } + } + if (nebulaSession != null) { + nebulaSession.release(); + sessionList.remove(nebulaSession); + } + return resultSet; + } + + public String executeJson(String stmt) + throws ClientServerIncompatibleException, AuthFailedException, + IOErrorException, BindSpaceFailedException { + return executeJsonWithParameter(stmt, Collections.emptyMap()); + } + + public String executeJsonWithParameter(String stmt, Map parameterMap) + throws ClientServerIncompatibleException, AuthFailedException, + IOErrorException, BindSpaceFailedException { + stmtCheck(stmt); + checkSessionPool(); + NebulaSession nebulaSession = getSession(); + String result; + try { + result = nebulaSession.executeJsonWithParameter(stmt, parameterMap); + if (isSessionErrorForJson(result)) { + sessionList.remove(nebulaSession); + nebulaSession = getSession(); + result = nebulaSession.executeJsonWithParameter(stmt, parameterMap); + } + } catch (IOErrorException e) { + if (nebulaSession != null) { + nebulaSession.release(); + sessionList.remove(nebulaSession); + } + throw e; + } + releaseSession(nebulaSession); + return result; + } + + public void close() { + if (isClosed.get()) { + return; + } + if (isClosed.compareAndSet(false, true)) { + for (NebulaSession nebulaSession : sessionList) { + nebulaSession.release(); + } + sessionList.clear(); + if (!healthCheckSchedule.isShutdown()) { + healthCheckSchedule.shutdown(); + } + if (!sessionQueueMaintainSchedule.isShutdown()) { + sessionQueueMaintainSchedule.shutdown(); + } + } + } + + public boolean isActive() { + return hasInit.get(); + } + + public boolean isClosed() { + return isClosed.get(); + } + + public int getSessionNums() { + return sessionList.size(); + } + + public int getIdleSessionNums() { + return idleSessionSize.get(); + } + + private void releaseSession(NebulaSession nebulaSession) { + nebulaSession.isUsedAndSetIdle(); + idleSessionSize.incrementAndGet(); + } + + private void checkSession() { + for (NebulaSession nebulaSession : sessionList) { + if (nebulaSession.isIdleAndSetUsed()) { + try { + idleSessionSize.decrementAndGet(); + nebulaSession.execute("RETURN 1"); + nebulaSession.isUsedAndSetIdle(); + idleSessionSize.incrementAndGet(); + } catch (IOErrorException e) { + log.error("session ping error, {}, remove current session.", e.getMessage()); + nebulaSession.release(); + sessionList.remove(nebulaSession); + } + } + } + } + + private void updateSessionQueue() { + if (idleSessionSize.get() > minSessionSize) { + synchronized (this) { + for (NebulaSession nebulaSession : sessionList) { + if (nebulaSession.isIdle()) { + nebulaSession.release(); + sessionList.remove(nebulaSession); + if (idleSessionSize.decrementAndGet() <= minSessionSize) { + break; + } + } + } + } + } + } + + private NebulaSession createSessionObject(SessionState state) + throws ClientServerIncompatibleException, AuthFailedException, + IOErrorException, BindSpaceFailedException { + com.vesoft.nebula.driver.graph.net.NebulaClient client = buildClient(); + + NebulaSession nebulaSession = new NebulaSession(client, state); + ResultSet result; + try { + result = nebulaSession.execute( + String.format("SESSION SET GRAPH \"%s\"", spaceName)); + } catch (IOErrorException e) { + log.error("binding graph failed,", e); + nebulaSession.release(); + throw new BindSpaceFailedException("binding graph failed:" + e.getMessage()); + } + if (!result.isSucceeded()) { + nebulaSession.release(); + throw new BindSpaceFailedException(result.getErrorMessage()); + } + sessionList.add(nebulaSession); + return nebulaSession; + } + + private com.vesoft.nebula.driver.graph.net.NebulaClient buildClient() + throws AuthFailedException, IOErrorException { + StringBuilder sb = new StringBuilder(); + for (HostAddress address : sessionPoolConfig.getGraphAddressList()) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(address.toString()); + } + com.vesoft.nebula.driver.graph.net.NebulaClient.Builder builder = + com.vesoft.nebula.driver.graph.net.NebulaClient.builder( + sb.toString(), sessionPoolConfig.getUsername(), sessionPoolConfig.getPassword()); + if (sessionPoolConfig.getTimeout() > 0) { + builder.withConnectTimeoutMills(sessionPoolConfig.getTimeout()); + builder.withRequestTimeoutMills(sessionPoolConfig.getTimeout()); + } + applyTls(builder, sessionPoolConfig); + try { + return builder.build(); + } catch (com.vesoft.nebula.driver.graph.exception.AuthFailedException e) { + throw Session.toCompatAuth(e); + } catch (com.vesoft.nebula.driver.graph.exception.IOErrorException e) { + throw Session.toCompat(e); + } + } + + private void applyTls(com.vesoft.nebula.driver.graph.net.NebulaClient.Builder builder, + SessionPoolConfig config) { + if (!config.isEnableSsl()) { + return; + } + builder.withEnableTls(true); + SSLParam ssl = config.getSslParam(); + if (ssl == null) { + builder.withDisableVerifyServerCert(true); + return; + } + if (ssl.isSkipVerifyServer()) { + builder.withDisableVerifyServerCert(true); + } + if (ssl instanceof CASignedSSLParam) { + CASignedSSLParam ca = (CASignedSSLParam) ssl; + if (ca.getCaCrtFilePath() != null) { + builder.withTlsCa(ca.getCaCrtFilePath()); + } + if (ca.getCrtFilePath() != null && ca.getKeyFilePath() != null) { + builder.withTlsCert(ca.getCrtFilePath(), ca.getKeyFilePath()); + } + } else if (ssl instanceof SelfSignedSSLParam) { + SelfSignedSSLParam self = (SelfSignedSSLParam) ssl; + builder.withDisableVerifyServerCert(true); + if (self.getCrtFilePath() != null && self.getKeyFilePath() != null) { + builder.withTlsCert(self.getCrtFilePath(), self.getKeyFilePath()); + } + } + } + + public HostAddress getAddress() { + List addresses = sessionPoolConfig.getGraphAddressList(); + int newPos = (pos.getAndIncrement()) % addresses.size(); + return addresses.get(newPos); + } + + private boolean isSessionError(ResultSet resultSet) { + return resultSet != null + && (resultSet.getErrorCode() == ErrorCode.E_SESSION_INVALID.getValue() + || resultSet.getErrorCode() == ErrorCode.E_SESSION_NOT_FOUND.getValue() + || resultSet.getErrorCode() == ErrorCode.E_SESSION_TIMEOUT.getValue()); + } + + private boolean isSessionErrorForJson(String result) { + if (result == null) { + return true; + } + int code = JSON.parseObject(result).getJSONArray("errors") + .getJSONObject(0).getIntValue("code"); + return code == ErrorCode.E_SESSION_INVALID.getValue() + || code == ErrorCode.E_SESSION_NOT_FOUND.getValue() + || code == ErrorCode.E_SESSION_TIMEOUT.getValue(); + } + + private void checkSessionPool() { + if (!hasInit.get()) { + throw new RuntimeException("The SessionPool has not been initialized, " + + "please call init() first."); + } + if (isClosed.get()) { + throw new RuntimeException("The SessionPool has been closed."); + } + } + + private void stmtCheck(String stmt) { + if (stmt == null || stmt.trim().isEmpty()) { + throw new IllegalArgumentException("statement is null."); + } + String trimmed = stmt.trim(); + if (trimmed.toLowerCase().startsWith("use ")) { + throw new IllegalArgumentException("`USE SPACE`/`USE GRAPH` alone is forbidden."); + } + if (trimmed.toLowerCase().startsWith("session set graph")) { + throw new IllegalArgumentException("`SESSION SET GRAPH` alone is forbidden."); + } + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/SessionPoolConfig.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/SessionPoolConfig.java new file mode 100644 index 000000000..469b127ed --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/SessionPoolConfig.java @@ -0,0 +1,254 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph; + +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.SSLParam; +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Session-pool configuration, matching the v3 client. + */ +public class SessionPoolConfig implements Serializable { + + private static final long serialVersionUID = -2266013330384849132L; + + private final List graphAddressList; + + private final String username; + private final String password; + private final String spaceName; + + private int minSessionSize = 1; + private int maxSessionSize = 10; + private int timeout = 0; + private int cleanTime = 3600; + private int healthCheckTime = 600; + private int retryConnectTimes = 1; + private int waitTime = 0; + private int retryTimes = 3; + private int intervalTime = 0; + private boolean reconnect = false; + private boolean enableSsl = false; + private SSLParam sslParam = null; + private boolean useHttp2 = false; + private Map customHeaders = new HashMap<>(); + + public SessionPoolConfig(List addresses, + String spaceName, + String username, + String password) { + if (addresses == null || addresses.size() == 0) { + throw new IllegalArgumentException("Graph addresses cannot be empty."); + } + if (spaceName == null || spaceName.trim().isEmpty()) { + throw new IllegalArgumentException("space name cannot be blank."); + } + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("user name cannot be blank."); + } + if (password == null || password.trim().isEmpty()) { + throw new IllegalArgumentException("password cannot be blank."); + } + this.graphAddressList = addresses; + this.spaceName = spaceName; + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public List getGraphAddressList() { + return graphAddressList; + } + + public String getSpaceName() { + return spaceName; + } + + public int getMinSessionSize() { + return minSessionSize; + } + + public SessionPoolConfig setMinSessionSize(int minSessionSize) { + if (minSessionSize < 1) { + throw new IllegalArgumentException("minSessionSize cannot be less than 1."); + } + this.minSessionSize = minSessionSize; + return this; + } + + public int getMaxSessionSize() { + return maxSessionSize; + } + + public SessionPoolConfig setMaxSessionSize(int maxSessionSize) { + if (maxSessionSize < 1) { + throw new IllegalArgumentException("maxSessionSize cannot be less than 1."); + } + this.maxSessionSize = maxSessionSize; + return this; + } + + public int getTimeout() { + return timeout; + } + + public SessionPoolConfig setTimeout(int timeout) { + if (timeout < 0) { + throw new IllegalArgumentException("timeout cannot be less than 0."); + } + this.timeout = timeout; + return this; + } + + public int getCleanTime() { + return cleanTime; + } + + public SessionPoolConfig setCleanTime(int cleanTime) { + if (cleanTime < 0) { + throw new IllegalArgumentException("cleanTime cannot be less than 0."); + } + this.cleanTime = cleanTime; + return this; + } + + public int getHealthCheckTime() { + return healthCheckTime; + } + + public SessionPoolConfig setHealthCheckTime(int healthCheckTime) { + if (healthCheckTime < 0) { + throw new IllegalArgumentException("healthCheckTime cannot be less than 0."); + } + this.healthCheckTime = healthCheckTime; + return this; + } + + public int getRetryConnectTimes() { + return retryConnectTimes; + } + + public SessionPoolConfig setRetryConnectTimes(int retryConnectTimes) { + if (retryConnectTimes < 0) { + throw new IllegalArgumentException("retryConnectTimes cannot be less than 0."); + } + this.retryConnectTimes = retryConnectTimes; + return this; + } + + public int getWaitTime() { + return waitTime; + } + + public SessionPoolConfig setWaitTime(int waitTime) { + if (waitTime < 0) { + throw new IllegalArgumentException("waitTime cannot be less than 0."); + } + this.waitTime = waitTime; + return this; + } + + public int getRetryTimes() { + return retryTimes; + } + + public SessionPoolConfig setRetryTimes(int retryTimes) { + if (retryTimes < 0) { + throw new IllegalArgumentException("retryTimes cannot be less than 0."); + } + this.retryTimes = retryTimes; + return this; + } + + public int getIntervalTime() { + return intervalTime; + } + + public SessionPoolConfig setIntervalTime(int intervalTime) { + if (intervalTime < 0) { + throw new IllegalArgumentException("intervalTime cannot be less than 0."); + } + this.intervalTime = intervalTime; + return this; + } + + public boolean isReconnect() { + return reconnect; + } + + public SessionPoolConfig setReconnect(boolean reconnect) { + this.reconnect = reconnect; + return this; + } + + public boolean isEnableSsl() { + return enableSsl; + } + + public SessionPoolConfig setEnableSsl(boolean enableSsl) { + this.enableSsl = enableSsl; + return this; + } + + public SSLParam getSslParam() { + return sslParam; + } + + public SessionPoolConfig setSslParam(SSLParam sslParam) { + this.sslParam = sslParam; + return this; + } + + public boolean isUseHttp2() { + return useHttp2; + } + + public SessionPoolConfig setUseHttp2(boolean useHttp2) { + this.useHttp2 = useHttp2; + return this; + } + + public Map getCustomHeaders() { + return customHeaders; + } + + public SessionPoolConfig setCustomHeaders(Map customHeaders) { + this.customHeaders = customHeaders; + return this; + } + + @Override + public String toString() { + return "SessionPoolConfig{" + + "username='" + username + '\'' + + ", graphAddressList=" + graphAddressList + + ", spaceName='" + spaceName + '\'' + + ", minSessionSize=" + minSessionSize + + ", maxSessionSize=" + maxSessionSize + + ", timeout=" + timeout + + ", cleanTime=" + cleanTime + + ", healthCheckTime=" + healthCheckTime + + ", retryTimes=" + retryTimes + + ", intervalTime=" + intervalTime + + ", reconnect=" + reconnect + + ", enableSsl=" + enableSsl + + ", sslParam=" + sslParam + + ", useHttp2=" + useHttp2 + + ", customHeaders=" + customHeaders + + '}'; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/SessionsManagerConfig.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/SessionsManagerConfig.java new file mode 100644 index 000000000..a8690ccc0 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/SessionsManagerConfig.java @@ -0,0 +1,79 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph; + +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Configuration for {@code SessionsManager}, matching the v3 client. + */ +public class SessionsManagerConfig implements Serializable { + + private static final long serialVersionUID = -2460063747630193912L; + + private List addresses = new ArrayList<>(); + private String userName = "root"; + private String password = "nebula"; + private String spaceName = ""; + private Boolean reconnect = true; + private NebulaPoolConfig poolConfig = new NebulaPoolConfig(); + + public List getAddresses() { + return addresses; + } + + public SessionsManagerConfig setAddresses(List addresses) { + this.addresses = addresses; + return this; + } + + public String getUserName() { + return userName; + } + + public SessionsManagerConfig setUserName(String userName) { + this.userName = userName; + return this; + } + + public String getPassword() { + return password; + } + + public SessionsManagerConfig setPassword(String password) { + this.password = password; + return this; + } + + public String getSpaceName() { + return spaceName; + } + + public SessionsManagerConfig setSpaceName(String spaceName) { + this.spaceName = spaceName; + return this; + } + + public Boolean getReconnect() { + return reconnect; + } + + public void setReconnect(Boolean reconnect) { + this.reconnect = reconnect; + } + + public NebulaPoolConfig getPoolConfig() { + return poolConfig; + } + + public SessionsManagerConfig setPoolConfig(NebulaPoolConfig poolConfig) { + this.poolConfig = poolConfig; + return this; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/BaseDataObject.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/BaseDataObject.java new file mode 100644 index 000000000..5c3945c76 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/BaseDataObject.java @@ -0,0 +1,35 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import java.io.Serializable; + +/** + * Base class for the wrapped graph data types, carrying the decode charset and timezone offset + * like the v3 client. + */ +public abstract class BaseDataObject implements Serializable { + private String decodeType = "utf-8"; + private int timezoneOffset = 0; + + public String getDecodeType() { + return decodeType; + } + + public BaseDataObject setDecodeType(String decodeType) { + this.decodeType = decodeType; + return this; + } + + public int getTimezoneOffset() { + return timezoneOffset; + } + + public BaseDataObject setTimezoneOffset(int timezoneOffset) { + this.timezoneOffset = timezoneOffset; + return this; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/CASignedSSLParam.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/CASignedSSLParam.java new file mode 100644 index 000000000..aaac415e0 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/CASignedSSLParam.java @@ -0,0 +1,38 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +/** + * CA-signed TLS configuration, matching the v3 client. + */ +public class CASignedSSLParam extends SSLParam { + private String caCrtFilePath; + private String crtFilePath; + private String keyFilePath; + + public CASignedSSLParam() { + super(SignMode.CA_SIGNED); + } + + public CASignedSSLParam(String caCrtFilePath, String crtFilePath, String keyFilePath) { + super(SignMode.CA_SIGNED); + this.caCrtFilePath = caCrtFilePath; + this.crtFilePath = crtFilePath; + this.keyFilePath = keyFilePath; + } + + public String getCaCrtFilePath() { + return caCrtFilePath; + } + + public String getCrtFilePath() { + return crtFilePath; + } + + public String getKeyFilePath() { + return keyFilePath; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/CoordinateWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/CoordinateWrapper.java new file mode 100644 index 000000000..c4b692280 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/CoordinateWrapper.java @@ -0,0 +1,50 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import com.vesoft.nebula.driver.graph.data.NPoint; +import java.util.Objects; + +/** + * Wrapper for a geographic coordinate (a point), matching the v3 client. + */ +public class CoordinateWrapper extends BaseDataObject { + private final NPoint point; + + public CoordinateWrapper(NPoint point) { + this.point = point; + } + + public double getX() { + return point.getLng(); + } + + public double getY() { + return point.getLat(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CoordinateWrapper that = (CoordinateWrapper) o; + return this.getX() == that.getX() && this.getY() == that.getY(); + } + + @Override + public String toString() { + return "COORDINATE(" + point.getLng() + " " + point.getLat() + ")"; + } + + @Override + public int hashCode() { + return Objects.hash(getX(), getY()); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/DateTimeWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/DateTimeWrapper.java new file mode 100644 index 000000000..613033404 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/DateTimeWrapper.java @@ -0,0 +1,144 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Objects; + +/** + * Wrapper for a datetime value, matching the v3 client. + */ +public class DateTimeWrapper extends BaseDataObject { + private final LocalDateTime utcDateTime; + + public DateTimeWrapper(LocalDateTime localDateTime) { + this.utcDateTime = localDateTime; + } + + public DateTimeWrapper(ZonedDateTime zonedDateTime) { + this.utcDateTime = zonedDateTime.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime(); + } + + /** + * @return utc datetime year + */ + public short getYear() { + return (short) utcDateTime.getYear(); + } + + /** + * @return utc datetime month + */ + public byte getMonth() { + return (byte) utcDateTime.getMonthValue(); + } + + /** + * @return utc datetime day + */ + public byte getDay() { + return (byte) utcDateTime.getDayOfMonth(); + } + + /** + * @return utc datetime hour + */ + public byte getHour() { + return (byte) utcDateTime.getHour(); + } + + /** + * @return utc datetime minute + */ + public byte getMinute() { + return (byte) utcDateTime.getMinute(); + } + + /** + * @return utc datetime second + */ + public byte getSecond() { + return (byte) utcDateTime.getSecond(); + } + + /** + * @return utc datetime microsec + */ + public int getMicrosec() { + return utcDateTime.getNano() / 1000; + } + + /** + * @return the local datetime ({@link LocalDateTime}) after applying + * {@link #getTimezoneOffset()}. + */ + public Object getLocalDateTime() { + return toLocalDateTime(getTimezoneOffset()); + } + + /** + * @return the datetime ({@link LocalDateTime}) with the specified timezone offset. + */ + public Object getDateTimeWithTimezoneOffset(int timezoneOffset) { + return toLocalDateTime(timezoneOffset); + } + + /** + * @return the local datetime string with the timezone offset applied. + */ + public String getLocalDateTimeStr() { + return format(toLocalDateTime(getTimezoneOffset())); + } + + /** + * @return the utc datetime string. + */ + public String getUTCDateTimeStr() { + return format(utcDateTime); + } + + private LocalDateTime toLocalDateTime(int timezoneOffset) { + if (timezoneOffset == 0) { + return utcDateTime; + } + return utcDateTime.atOffset(ZoneOffset.UTC) + .withOffsetSameInstant(ZoneOffset.ofTotalSeconds(timezoneOffset)) + .toLocalDateTime(); + } + + private String format(LocalDateTime dateTime) { + return String.format("%d-%02d-%02dT%02d:%02d:%02d.%06d", + dateTime.getYear(), dateTime.getMonthValue(), + dateTime.getDayOfMonth(), dateTime.getHour(), + dateTime.getMinute(), dateTime.getSecond(), + dateTime.getNano() / 1000); + } + + @Override + public String toString() { + return String.format("utc datetime: %s, timezoneOffset: %d", getUTCDateTimeStr(), + getTimezoneOffset()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DateTimeWrapper that = (DateTimeWrapper) o; + return utcDateTime.equals(that.utcDateTime); + } + + @Override + public int hashCode() { + return Objects.hash(utcDateTime); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/DateWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/DateWrapper.java new file mode 100644 index 000000000..03fe7d941 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/DateWrapper.java @@ -0,0 +1,55 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import java.time.LocalDate; +import java.util.Objects; + +/** + * Wrapper for a date value, matching the v3 client. + */ +public class DateWrapper extends BaseDataObject { + private final LocalDate date; + + public DateWrapper(LocalDate date) { + this.date = date; + } + + public short getYear() { + return (short) date.getYear(); + } + + public byte getMonth() { + return (byte) date.getMonthValue(); + } + + public byte getDay() { + return (byte) date.getDayOfMonth(); + } + + @Override + public String toString() { + return String.format("%d-%02d-%02d", date.getYear(), date.getMonthValue(), + date.getDayOfMonth()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DateWrapper that = (DateWrapper) o; + return date.equals(that.date); + } + + @Override + public int hashCode() { + return Objects.hash(date); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/DurationWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/DurationWrapper.java new file mode 100644 index 000000000..5a7cf0cda --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/DurationWrapper.java @@ -0,0 +1,82 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import com.vesoft.nebula.driver.graph.data.NDuration; +import java.util.Objects; + +/** + * Wrapper for a duration value, matching the v3 client. + * + *

The v5 duration carries a richer field set (years/months/days/hours/minutes/seconds/ + * microseconds); it is folded into the v3 three-field model ({@code months}/{@code seconds}/ + * {@code microseconds}) on a best-effort basis. + */ +public class DurationWrapper extends BaseDataObject { + private final NDuration duration; + + public DurationWrapper(NDuration duration) { + this.duration = duration; + } + + /** + * @return the seconds part of the duration. + */ + public long getSeconds() { + if (duration.isMonthBased()) { + return 0; + } + return duration.getDay() * 86400L + duration.getHour() * 3600L + + duration.getMinute() * 60L + duration.getSecond(); + } + + /** + * @return the microseconds part of the duration. + */ + public int getMicroseconds() { + return duration.getMicrosecond(); + } + + /** + * @return the months part of the duration. + */ + public int getMonths() { + if (!duration.isMonthBased()) { + return 0; + } + return duration.getYear() * 12 + duration.getMonth(); + } + + /** + * @return the duration string. + */ + public String getDurationString() { + return String.format("duration({months:%d, seconds:%d, microseconds:%d})", + getMonths(), getSeconds(), getMicroseconds()); + } + + @Override + public String toString() { + return duration.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DurationWrapper that = (DurationWrapper) o; + return duration.equals(that.duration); + } + + @Override + public int hashCode() { + return Objects.hash(duration); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/GeographyWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/GeographyWrapper.java new file mode 100644 index 000000000..2738465e1 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/GeographyWrapper.java @@ -0,0 +1,65 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import com.vesoft.nebula.driver.graph.data.Geography; +import com.vesoft.nebula.driver.graph.data.Geography.GeoShape; +import java.util.Objects; + +/** + * Wrapper for a geography value, matching the v3 client. + */ +public class GeographyWrapper extends BaseDataObject { + private final Geography geography; + + public GeographyWrapper(Geography geography) { + this.geography = geography; + } + + public PolygonWrapper getPolygonWrapper() { + return new PolygonWrapper(geography.asPolygon()); + } + + public LineStringWrapper getLineStringWrapper() { + return new LineStringWrapper(geography.asLineString()); + } + + public PointWrapper getPointWrapper() { + return new PointWrapper(geography.asPoint()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GeographyWrapper that = (GeographyWrapper) o; + return geography.toString().equals(that.geography.toString()); + } + + @Override + public String toString() { + GeoShape shape = geography.getShape(); + switch (shape) { + case GeoShapePoint: + return getPointWrapper().toString(); + case GeoShapeLineString: + return getLineStringWrapper().toString(); + case GeoShapePolygon: + return getPolygonWrapper().toString(); + default: + return ""; + } + } + + @Override + public int hashCode() { + return Objects.hash(geography); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/HostAddress.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/HostAddress.java new file mode 100644 index 000000000..8c7ce7356 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/HostAddress.java @@ -0,0 +1,71 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import java.io.Serializable; + +/** + * A graphd host address (host + port), matching the v3 client. + */ +public class HostAddress implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String host; + private final int port; + + public HostAddress(String host, int port) { + this.host = host; + this.port = port; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + @Override + public int hashCode() { + return host.hashCode() + port; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof HostAddress) { + HostAddress that = (HostAddress) obj; + return this.host.equals(that.host) && this.port == that.port; + } + return false; + } + + @Override + public String toString() { + if (host.contains(":")) { + return "[" + host + "]:" + port; + } + return host + ":" + port; + } + + /** + * Convert to the v5 driver's host address type. + */ + public com.vesoft.nebula.driver.graph.data.HostAddress toV5() { + return new com.vesoft.nebula.driver.graph.data.HostAddress(host, port); + } + + /** + * Convert from the v5 driver's host address type. + */ + public static HostAddress fromV5(com.vesoft.nebula.driver.graph.data.HostAddress address) { + return new HostAddress(address.getHost(), address.getPort()); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/LineStringWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/LineStringWrapper.java new file mode 100644 index 000000000..4834edff1 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/LineStringWrapper.java @@ -0,0 +1,64 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import com.vesoft.nebula.driver.graph.data.NLineString; +import com.vesoft.nebula.driver.graph.data.NPoint; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Wrapper for a geographic line string, matching the v3 client. + */ +public class LineStringWrapper extends BaseDataObject { + private final NLineString lineString; + + public LineStringWrapper(NLineString lineString) { + this.lineString = lineString; + } + + public List getCoordinateList() { + List coordList = new ArrayList<>(); + for (NPoint point : lineString.getPoints()) { + coordList.add(new CoordinateWrapper(point)); + } + return coordList; + } + + @Override + public int hashCode() { + return Objects.hash(lineString); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LineStringWrapper that = (LineStringWrapper) o; + return this.getCoordinateList().equals(that.getCoordinateList()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("LINESTRING("); + List points = lineString.getPoints(); + for (int i = 0; i < points.size(); i++) { + NPoint point = points.get(i); + sb.append(point.getLng()).append(' ').append(point.getLat()); + if (i < points.size() - 1) { + sb.append(','); + } + } + sb.append(')'); + return sb.toString(); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/Node.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/Node.java new file mode 100644 index 000000000..46a2c106f --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/Node.java @@ -0,0 +1,124 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Wrapper around a graph node (vertex), matching the v3 client's {@code Node} API. + * + *

The v5 driver models a node as a single node type with a flat property map plus a list of + * labels, so the v3 per-tag accessors ({@link #values(String)}, {@link #keys(String)}, + * {@link #properties(String)}) validate the given label and return the flat property set. + */ +public class Node extends BaseDataObject { + + private final com.vesoft.nebula.driver.graph.data.Node node; + + public Node(com.vesoft.nebula.driver.graph.data.Node node) { + if (node == null) { + throw new RuntimeException("Input an null node object"); + } + this.node = node; + } + + /** + * @return the node id as a {@link ValueWrapper}; call {@code getId().asLong()}. + */ + public ValueWrapper getId() { + return ValueWrapper.ofLong(node.getId()); + } + + /** + * @return the node type name(s) exposed as labels. + */ + public List tagNames() { + return new ArrayList<>(node.getLabels()); + } + + /** + * @return the labels of the node (alias of {@link #tagNames()}). + */ + public List labels() { + return node.getLabels(); + } + + public boolean hasTagName(String tagName) { + return node.getLabels().contains(tagName); + } + + public boolean hasLabel(String tagName) { + return node.getLabels().contains(tagName); + } + + public List values(String tagName) { + checkTagName(tagName); + return new ArrayList<>(propertiesFor(tagName).values()); + } + + public List keys(String tagName) throws UnsupportedEncodingException { + checkTagName(tagName); + return new ArrayList<>(propertiesFor(tagName).keySet()); + } + + public HashMap properties(String tagName) + throws UnsupportedEncodingException { + checkTagName(tagName); + return propertiesFor(tagName); + } + + private void checkTagName(String tagName) { + if (!node.getLabels().contains(tagName)) { + throw new IllegalArgumentException(tagName + " is not found"); + } + } + + private HashMap propertiesFor(String tagName) { + HashMap properties = new HashMap<>(); + for (Map.Entry entry + : node.getProperties().entrySet()) { + properties.put(entry.getKey(), new ValueWrapper(entry.getValue(), getTimezoneOffset())); + } + return properties; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Node node = (Node) o; + return Objects.equals(this.node.getId(), node.node.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(node.getId(), getDecodeType(), getTimezoneOffset()); + } + + @Override + public String toString() { + List tagsStr = new ArrayList<>(); + Map props = node.getProperties(); + List propStrs = new ArrayList<>(); + for (Map.Entry entry + : props.entrySet()) { + propStrs.add(entry.getKey() + ": " + entry.getValue().toString()); + } + for (String name : node.getLabels()) { + tagsStr.add(String.format(":%s {%s}", name, String.join(", ", propStrs))); + } + return String.format("(%d %s)", node.getId(), String.join(" ", tagsStr)); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/PathWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/PathWrapper.java new file mode 100644 index 000000000..0590121eb --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/PathWrapper.java @@ -0,0 +1,167 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import com.vesoft.nebula.driver.v3client.graph.exception.InvalidValueException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Wrapper around a graph path, matching the v3 client's {@code PathWrapper} API. + */ +public class PathWrapper extends BaseDataObject { + + public static class Segment { + Node startNode; + Relationship relationShip; + Node endNode; + + public Segment(Node startNode, Relationship relationShip, Node endNode) { + this.startNode = startNode; + this.relationShip = relationShip; + this.endNode = endNode; + } + + public Node getStartNode() { + return startNode; + } + + public Relationship getRelationShip() { + return relationShip; + } + + public Node getEndNode() { + return endNode; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Segment segment = (Segment) o; + return Objects.equals(startNode, segment.startNode) + && Objects.equals(relationShip, segment.relationShip) + && Objects.equals(endNode, segment.endNode); + } + + @Override + public int hashCode() { + return Objects.hash(startNode, relationShip, endNode); + } + + @Override + public String toString() { + return "Segment{" + "startNode=" + startNode + ", relationShip=" + relationShip + + ", endNode=" + endNode + '}'; + } + } + + private final List segments = new ArrayList<>(); + private final List nodes = new ArrayList<>(); + private final List relationships = new ArrayList<>(); + + public PathWrapper(com.vesoft.nebula.driver.graph.data.Path path, int timezoneOffset) + throws InvalidValueException { + setTimezoneOffset(timezoneOffset); + if (path == null) { + return; + } + for (com.vesoft.nebula.driver.graph.data.Node node : path.nodes()) { + nodes.add((Node) new Node(node).setTimezoneOffset(timezoneOffset)); + } + for (com.vesoft.nebula.driver.graph.data.Edge edge : path.edges()) { + relationships.add((Relationship) new Relationship(edge).setTimezoneOffset( + timezoneOffset)); + } + for (int i = 0; i < relationships.size(); i++) { + if (i + 1 >= nodes.size()) { + throw new InvalidValueException("Malformed path: not enough nodes for edges"); + } + segments.add(new Segment(nodes.get(i), relationships.get(i), nodes.get(i + 1))); + } + } + + public Node getStartNode() { + if (nodes == null || nodes.isEmpty()) { + return null; + } + return nodes.get(0); + } + + public Node getEndNode() { + if (nodes == null || nodes.isEmpty()) { + return null; + } + return nodes.get(nodes.size() - 1); + } + + public boolean containNode(Node node) { + return nodes.contains(node); + } + + public boolean containRelationship(Relationship relationship) { + return relationships.contains(relationship); + } + + public List getNodes() { + return nodes; + } + + public List getRelationships() { + return relationships; + } + + public List getSegments() { + return segments; + } + + public int length() { + return segments.size(); + } + + @Override + public String toString() { + if (nodes.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append(nodes.get(0).toString()); + for (int i = 0; i < relationships.size(); i++) { + Relationship relationship = relationships.get(i); + sb.append("-[:").append(relationship.edgeName()).append('@') + .append(relationship.ranking()).append("{}]->"); + if (i + 1 < nodes.size()) { + sb.append(nodes.get(i + 1).toString()); + } + } + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PathWrapper that = (PathWrapper) o; + return Objects.equals(segments, that.segments) + && Objects.equals(nodes, that.nodes) + && Objects.equals(relationships, that.relationships); + } + + @Override + public int hashCode() { + return Objects.hash(segments, nodes, relationships); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/PointWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/PointWrapper.java new file mode 100644 index 000000000..1d654fc17 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/PointWrapper.java @@ -0,0 +1,46 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import com.vesoft.nebula.driver.graph.data.NPoint; +import java.util.Objects; + +/** + * Wrapper for a geographic point, matching the v3 client. + */ +public class PointWrapper extends BaseDataObject { + private final NPoint point; + + public PointWrapper(NPoint point) { + this.point = point; + } + + public CoordinateWrapper getCoordinate() { + return new CoordinateWrapper(point); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PointWrapper that = (PointWrapper) o; + return this.getCoordinate().equals(that.getCoordinate()); + } + + @Override + public String toString() { + return "POINT(" + point.getLng() + " " + point.getLat() + ")"; + } + + @Override + public int hashCode() { + return Objects.hash(point.getLng(), point.getLat()); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/PolygonWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/PolygonWrapper.java new file mode 100644 index 000000000..3cb604f60 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/PolygonWrapper.java @@ -0,0 +1,76 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import com.vesoft.nebula.driver.graph.data.NPoint; +import com.vesoft.nebula.driver.graph.data.NPolygon; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Wrapper for a geographic polygon, matching the v3 client. + */ +public class PolygonWrapper extends BaseDataObject { + private final NPolygon polygon; + + public PolygonWrapper(NPolygon polygon) { + this.polygon = polygon; + } + + public List> getCoordListList() { + List> coordListList = new ArrayList<>(); + for (List loop : polygon.getLoops()) { + List coordList = new ArrayList<>(); + for (NPoint point : loop) { + coordList.add(new CoordinateWrapper(point)); + } + coordListList.add(coordList); + } + return coordListList; + } + + @Override + public int hashCode() { + return Objects.hash(polygon); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PolygonWrapper that = (PolygonWrapper) o; + return this.getCoordListList().equals(that.getCoordListList()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("POLYGON("); + List> loops = polygon.getLoops(); + for (int i = 0; i < loops.size(); i++) { + sb.append('('); + List loop = loops.get(i); + for (int j = 0; j < loop.size(); j++) { + NPoint point = loop.get(j); + sb.append(point.getLng()).append(' ').append(point.getLat()); + if (j < loop.size() - 1) { + sb.append(','); + } + } + sb.append(')'); + if (i < loops.size() - 1) { + sb.append(','); + } + } + sb.append(')'); + return sb.toString(); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/Relationship.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/Relationship.java new file mode 100644 index 000000000..99320c8e2 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/Relationship.java @@ -0,0 +1,114 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Wrapper around a graph relationship (edge), matching the v3 client's {@code Relationship} API. + */ +public class Relationship extends BaseDataObject { + + private final com.vesoft.nebula.driver.graph.data.Edge edge; + + public Relationship(com.vesoft.nebula.driver.graph.data.Edge edge) { + if (edge == null) { + throw new RuntimeException("Input an null edge object"); + } + this.edge = edge; + } + + /** + * @return the source id as a {@link ValueWrapper}. + */ + public ValueWrapper srcId() { + return ValueWrapper.ofLong(edge.getSrcId()); + } + + /** + * @return the destination id as a {@link ValueWrapper}. + */ + public ValueWrapper dstId() { + return ValueWrapper.ofLong(edge.getDstId()); + } + + /** + * @return the edge name. The v3 client used the edge label as the name; the v5 driver stores + * the label(s) separately from the edge type name, so prefer the first label. + */ + public String edgeName() { + if (edge.getLabels() != null && !edge.getLabels().isEmpty()) { + return edge.getLabels().get(0); + } + return edge.getType(); + } + + /** + * @return the rank of the edge. + */ + public long ranking() { + return edge.getRank(); + } + + public List keys() throws UnsupportedEncodingException { + return new ArrayList<>(edge.getColumnNames()); + } + + public List values() { + List propVals = new ArrayList<>(); + for (com.vesoft.nebula.driver.graph.data.ValueWrapper val : edge.getPropertyValues()) { + propVals.add(new ValueWrapper(val, getTimezoneOffset())); + } + return propVals; + } + + public HashMap properties() throws UnsupportedEncodingException { + HashMap properties = new HashMap<>(); + for (Map.Entry entry + : edge.getProperties().entrySet()) { + properties.put(entry.getKey(), new ValueWrapper(entry.getValue(), getTimezoneOffset())); + } + return properties; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Relationship that = (Relationship) o; + return edge.getRank() == that.edge.getRank() + && edge.getSrcId() == that.edge.getSrcId() + && edge.getDstId() == that.edge.getDstId() + && Objects.equals(edge.getType(), that.edge.getType()); + } + + @Override + public int hashCode() { + return Objects.hash(edge.getType(), edge.getRank(), edge.getSrcId(), edge.getDstId(), + getDecodeType(), getTimezoneOffset()); + } + + @Override + public String toString() { + List propStrs = new ArrayList<>(); + for (Map.Entry entry + : edge.getProperties().entrySet()) { + propStrs.add(entry.getKey() + ": " + entry.getValue().toString()); + } + return String.format("(%d)-[:%s@%d{%s}]->(%d)", + edge.getSrcId(), edge.getType(), edge.getRank(), + String.join(", ", propStrs), edge.getDstId()); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/ResultSet.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/ResultSet.java new file mode 100644 index 000000000..d25ca27ef --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/ResultSet.java @@ -0,0 +1,209 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import com.vesoft.nebula.driver.v3client.graph.ErrorCode; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.function.Consumer; + +/** + * The result of a query, matching the v3 client's {@code ResultSet} API. + * + *

The v5 driver returns a one-shot forward iterator; this wrapper eagerly materializes all rows + * so that the v3 index-based accessors ({@link #rowValues(int)}, {@link #colValues(String)}) and + * repeated iteration keep working. + */ +public class ResultSet { + + public static class Record implements Iterable { + + private final List colValues = new ArrayList<>(); + private final List columnNames; + + public Record(List columnNames, + com.vesoft.nebula.driver.graph.data.ResultSet.Record record, + int timezoneOffset) { + this.columnNames = columnNames; + if (record == null) { + return; + } + for (com.vesoft.nebula.driver.graph.data.ValueWrapper value : record.values()) { + this.colValues.add(new ValueWrapper(value, timezoneOffset)); + } + } + + @Override + public Iterator iterator() { + return this.colValues.iterator(); + } + + @Override + public void forEach(Consumer action) { + this.colValues.forEach(action); + } + + @Override + public Spliterator spliterator() { + return this.colValues.spliterator(); + } + + @Override + public String toString() { + List valueStr = new ArrayList<>(); + for (ValueWrapper v : colValues) { + valueStr.add(v.toString()); + } + return String.format("ColumnName: %s, Values: %s", + columnNames.toString(), valueStr.toString()); + } + + public ValueWrapper get(int index) { + if (index >= columnNames.size()) { + throw new IllegalArgumentException( + String.format("Cannot get field because the key '%d' out of range", index)); + } + return this.colValues.get(index); + } + + public ValueWrapper get(String columnName) { + int index = columnNames.indexOf(columnName); + if (index == -1) { + throw new IllegalArgumentException( + "Cannot get field because the columnName '" + + columnName + "' is not exists"); + } + return this.colValues.get(index); + } + + public List values() { + return colValues; + } + + public int size() { + return this.columnNames.size(); + } + + public boolean contains(String columnName) { + return this.columnNames.contains(columnName); + } + } + + private final List columnNames = new ArrayList<>(); + private final List records = new ArrayList<>(); + private final int timezoneOffset; + private final boolean succeeded; + private final boolean empty; + private final int errorCode; + private final String errorMessage; + private final long latency; + private final com.vesoft.nebula.driver.graph.data.PlanInfoNode planDesc; + + public ResultSet(com.vesoft.nebula.driver.graph.data.ResultSet resultSet) { + this(resultSet, 0); + } + + public ResultSet(com.vesoft.nebula.driver.graph.data.ResultSet resultSet, int timezoneOffset) { + if (resultSet == null) { + throw new RuntimeException("Input an null `ResultSet' object"); + } + this.timezoneOffset = timezoneOffset; + this.succeeded = resultSet.isSucceeded(); + this.empty = resultSet.isEmpty(); + this.errorCode = ErrorCode.fromV5ErrorCode(resultSet.getErrorCode()); + this.errorMessage = resultSet.getErrorMessage(); + this.latency = resultSet.getLatency(); + this.planDesc = resultSet.getPlanDesc(); + this.columnNames.addAll(resultSet.getColumnNames()); + while (resultSet.hasNext()) { + records.add(new Record(columnNames, resultSet.next(), timezoneOffset)); + } + } + + public boolean isSucceeded() { + return succeeded; + } + + public boolean isEmpty() { + return empty; + } + + public int getErrorCode() { + return errorCode; + } + + public String getSpaceName() { + return ""; + } + + public String getErrorMessage() { + return errorMessage; + } + + public String getComment() { + return ""; + } + + public long getLatency() { + return latency; + } + + public com.vesoft.nebula.driver.graph.data.PlanInfoNode getPlanDesc() { + return planDesc; + } + + public List keys() { + return columnNames; + } + + public List getColumnNames() { + return columnNames; + } + + public int rowsSize() { + return records.size(); + } + + public Record rowValues(int index) { + if (index >= records.size()) { + throw new ArrayIndexOutOfBoundsException(); + } + return records.get(index); + } + + public List colValues(String columnName) { + int index = columnNames.indexOf(columnName); + if (index < 0) { + throw new ArrayIndexOutOfBoundsException(); + } + List values = new ArrayList<>(); + for (Record record : records) { + values.add(record.get(index)); + } + return values; + } + + @Override + public String toString() { + if (!isSucceeded()) { + return getErrorMessage(); + } + int i = 0; + List rowStrs = new ArrayList<>(); + while (i < rowsSize()) { + List valueStrs = new ArrayList<>(); + for (ValueWrapper value : rowValues(i)) { + valueStrs.add(value.toString()); + } + rowStrs.add(String.join(",", valueStrs)); + i++; + } + return String.format("ColumnName: %s, Rows: %s", + columnNames.toString(), rowStrs.toString()); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/SSLParam.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/SSLParam.java new file mode 100644 index 000000000..d3e8da566 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/SSLParam.java @@ -0,0 +1,42 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import java.io.Serializable; + +/** + * Base class for TLS configuration, matching the v3 client. + */ +public abstract class SSLParam implements Serializable { + + private static final long serialVersionUID = 7410233298826490747L; + + private boolean skipVerifyServer = false; + + public enum SignMode { + NONE, + SELF_SIGNED, + CA_SIGNED + } + + private SignMode signMode; + + public boolean isSkipVerifyServer() { + return skipVerifyServer; + } + + public void setSkipVerifyServer(boolean skipVerifyServer) { + this.skipVerifyServer = skipVerifyServer; + } + + public SSLParam(SignMode signMode) { + this.signMode = signMode; + } + + public SignMode getSignMode() { + return signMode; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/SelfSignedSSLParam.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/SelfSignedSSLParam.java new file mode 100644 index 000000000..d3d977c91 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/SelfSignedSSLParam.java @@ -0,0 +1,34 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +/** + * Self-signed TLS configuration, matching the v3 client. + */ +public class SelfSignedSSLParam extends SSLParam { + private String crtFilePath; + private String keyFilePath; + private String password; + + public SelfSignedSSLParam(String crtFilePath, String keyFilePath, String password) { + super(SignMode.SELF_SIGNED); + this.crtFilePath = crtFilePath; + this.keyFilePath = keyFilePath; + this.password = password; + } + + public String getCrtFilePath() { + return crtFilePath; + } + + public String getKeyFilePath() { + return keyFilePath; + } + + public String getPassword() { + return password; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/TimeUtil.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/TimeUtil.java new file mode 100644 index 000000000..87b81b1be --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/TimeUtil.java @@ -0,0 +1,48 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneOffset; + +/** + * Timezone conversion helpers used by the compatibility wrappers. + * + *

The v3 client operated on Thrift time structs; the compatibility layer operates directly on + * {@code java.time} types, so these helpers have matching names but java.time signatures. + */ +public class TimeUtil { + + /** + * @param utcDateTime the utc datetime + * @param timezoneOffset the timezone offset, unit is seconds + * @return the datetime shifted to the timezone offset + */ + public static LocalDateTime datetimeConvertWithTimezone(LocalDateTime utcDateTime, + int timezoneOffset) { + if (timezoneOffset == 0) { + return utcDateTime; + } + return utcDateTime.atOffset(ZoneOffset.UTC) + .withOffsetSameInstant(ZoneOffset.ofTotalSeconds(timezoneOffset)) + .toLocalDateTime(); + } + + /** + * @param utcTime the utc time + * @param timezoneOffset the timezone offset, unit is seconds + * @return the time shifted to the timezone offset + */ + public static LocalTime timeConvertWithTimezone(LocalTime utcTime, int timezoneOffset) { + if (timezoneOffset == 0) { + return utcTime; + } + return utcTime.atOffset(ZoneOffset.UTC) + .withOffsetSameInstant(ZoneOffset.ofTotalSeconds(timezoneOffset)) + .toLocalTime(); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/TimeWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/TimeWrapper.java new file mode 100644 index 000000000..1a2e806bb --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/TimeWrapper.java @@ -0,0 +1,123 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.util.Objects; + +/** + * Wrapper for a time value, matching the v3 client. + * + *

The stored time is normalized to UTC; the local-time helpers apply the + * {@link #getTimezoneOffset() timezone offset} inherited from {@link BaseDataObject}. + */ +public class TimeWrapper extends BaseDataObject { + private final LocalTime utcTime; + + public TimeWrapper(LocalTime localTime) { + this.utcTime = localTime; + } + + public TimeWrapper(OffsetTime offsetTime) { + this.utcTime = offsetTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalTime(); + } + + /** + * @return utc Time hour + */ + public byte getHour() { + return (byte) utcTime.getHour(); + } + + /** + * @return utc Time minute + */ + public byte getMinute() { + return (byte) utcTime.getMinute(); + } + + /** + * @return utc Time second + */ + public byte getSecond() { + return (byte) utcTime.getSecond(); + } + + /** + * @return utc Time microsec + */ + public int getMicrosec() { + return utcTime.getNano() / 1000; + } + + /** + * @return the local time ({@link LocalTime}) after applying {@link #getTimezoneOffset()}. + */ + public Object getLocalTime() { + return toLocalTime(getTimezoneOffset()); + } + + /** + * @return the time ({@link LocalTime}) with the specified timezone offset. + */ + public Object getTimeWithTimezoneOffset(int timezoneOffset) { + return toLocalTime(timezoneOffset); + } + + /** + * @return the local time string with the timezone offset applied. + */ + public String getLocalTimeStr() { + return format(toLocalTime(getTimezoneOffset())); + } + + /** + * @return the utc time string. + */ + public String getUTCTimeStr() { + return format(utcTime); + } + + private LocalTime toLocalTime(int timezoneOffset) { + if (timezoneOffset == 0) { + return utcTime; + } + return utcTime.atOffset(ZoneOffset.UTC) + .withOffsetSameInstant(ZoneOffset.ofTotalSeconds(timezoneOffset)) + .toLocalTime(); + } + + private String format(LocalTime time) { + return String.format("%02d:%02d:%02d.%06d", + time.getHour(), time.getMinute(), time.getSecond(), + time.getNano() / 1000); + } + + @Override + public String toString() { + return String.format("utc time: %s, timezoneOffset: %d", getUTCTimeStr(), + getTimezoneOffset()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TimeWrapper that = (TimeWrapper) o; + return utcTime.equals(that.utcTime); + } + + @Override + public int hashCode() { + return Objects.hash(utcTime); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/ValueWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/ValueWrapper.java new file mode 100644 index 000000000..535a62086 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/data/ValueWrapper.java @@ -0,0 +1,396 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import com.vesoft.nebula.driver.graph.decode.ColumnType; +import com.vesoft.nebula.driver.v3client.graph.exception.InvalidValueException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Wrapper around a NebulaGraph value, matching the v3 client's {@code ValueWrapper} API. + * + *

Internally it wraps a v5 driver {@link com.vesoft.nebula.driver.graph.data.ValueWrapper}. + */ +public class ValueWrapper { + + public static class NullType { + public static final int __NULL__ = 0; + public static final int NaN = 1; + public static final int BAD_DATA = 2; + public static final int BAD_TYPE = 3; + public static final int ERR_OVERFLOW = 4; + public static final int UNKNOWN_PROP = 5; + public static final int DIV_BY_ZERO = 6; + public static final int OUT_OF_RANGE = 7; + int nullType; + + public NullType(int nullType) { + this.nullType = nullType; + } + + public int getNullType() { + return nullType; + } + + @Override + public String toString() { + switch (nullType) { + case __NULL__: + return "NULL"; + case NaN: + return "NaN"; + case BAD_DATA: + return "BAD_DATA"; + case BAD_TYPE: + return "BAD_TYPE"; + case ERR_OVERFLOW: + return "ERR_OVERFLOW"; + case UNKNOWN_PROP: + return "UNKNOWN_PROP"; + case DIV_BY_ZERO: + return "DIV_BY_ZERO"; + case OUT_OF_RANGE: + return "OUT_OF_RANGE"; + default: + return "Unknown type: " + nullType; + } + } + } + + private final com.vesoft.nebula.driver.graph.data.ValueWrapper value; + private final int timezoneOffset; + + public ValueWrapper(com.vesoft.nebula.driver.graph.data.ValueWrapper value) { + this(value, 0); + } + + public ValueWrapper(com.vesoft.nebula.driver.graph.data.ValueWrapper value, + int timezoneOffset) { + this.value = value; + this.timezoneOffset = timezoneOffset; + } + + /** + * Build a compatibility value wrapping a {@code long} (used for node/edge ids). + */ + public static ValueWrapper ofLong(long value) { + return new ValueWrapper(new com.vesoft.nebula.driver.graph.data.ValueWrapper( + value, ColumnType.COLUMN_TYPE_INT64)); + } + + /** + * @return the underlying v5 value wrapper. + */ + public Object getValue() { + return value; + } + + /** + * The v5 driver has no distinct "empty" type; always returns {@code false}. + */ + public boolean isEmpty() { + return false; + } + + public boolean isNull() { + return value.isNull(); + } + + public boolean isBoolean() { + return value.isBoolean(); + } + + public boolean isLong() { + return value.isLong() || value.isInt(); + } + + public boolean isDouble() { + return value.isDouble() || value.isFloat() || value.isDecimal(); + } + + public boolean isString() { + return value.isString(); + } + + public boolean isList() { + return value.isList(); + } + + public boolean isSet() { + return value.isSet(); + } + + public boolean isMap() { + return value.isMap(); + } + + public boolean isTime() { + return value.isLocalTime() || value.isZonedTime(); + } + + public boolean isDate() { + return value.isDate(); + } + + public boolean isDateTime() { + return value.isLocalDateTime() || value.isZonedDateTime(); + } + + public boolean isVertex() { + return value.isNode(); + } + + public boolean isEdge() { + return value.isEdge(); + } + + public boolean isPath() { + return value.isPath(); + } + + public boolean isGeography() { + return value.isGeography(); + } + + public boolean isDuration() { + return value.isDuration(); + } + + public NullType asNull() throws InvalidValueException { + if (value.isNull()) { + return new NullType(NullType.__NULL__); + } + throw new InvalidValueException( + "Cannot get field nullType because value's type is " + value.getDataTypeString()); + } + + public boolean asBoolean() throws InvalidValueException { + if (value.isBoolean()) { + return value.asBoolean(); + } + throw new InvalidValueException( + "Cannot get field boolean because value's type is " + value.getDataTypeString()); + } + + public long asLong() throws InvalidValueException { + if (value.isLong()) { + return value.asLong(); + } + if (value.isInt()) { + return value.asInt(); + } + throw new InvalidValueException( + "Cannot get field long because value's type is " + value.getDataTypeString()); + } + + public String asString() throws InvalidValueException, UnsupportedEncodingException { + if (value.isString()) { + return value.asString(); + } + throw new InvalidValueException( + "Cannot get field string because value's type is " + value.getDataTypeString()); + } + + public double asDouble() throws InvalidValueException { + if (value.isDouble()) { + return value.asDouble(); + } + if (value.isFloat()) { + return value.asFloat(); + } + if (value.isDecimal()) { + return value.asDecimal().doubleValue(); + } + throw new InvalidValueException( + "Cannot get field double because value's type is " + value.getDataTypeString()); + } + + public ArrayList asList() throws InvalidValueException { + if (value.isList()) { + ArrayList values = new ArrayList<>(); + for (com.vesoft.nebula.driver.graph.data.ValueWrapper element : value.asList()) { + values.add(new ValueWrapper(element, timezoneOffset)); + } + return values; + } + throw new InvalidValueException( + "Cannot get field `list' because value's type is " + value.getDataTypeString()); + } + + public HashSet asSet() throws InvalidValueException { + if (value.isSet()) { + HashSet values = new HashSet<>(); + Set set = value.asSet(); + for (Object element : set) { + values.add(new ValueWrapper( + (com.vesoft.nebula.driver.graph.data.ValueWrapper) element, timezoneOffset)); + } + return values; + } + throw new InvalidValueException( + "Cannot get field `set' because value's type is " + value.getDataTypeString()); + } + + public HashMap asMap() + throws InvalidValueException, UnsupportedEncodingException { + if (value.isMap()) { + HashMap kvs = new HashMap<>(); + Map map = value.asMap(); + for (Map.Entry entry : map.entrySet()) { + com.vesoft.nebula.driver.graph.data.ValueWrapper key = + (com.vesoft.nebula.driver.graph.data.ValueWrapper) entry.getKey(); + com.vesoft.nebula.driver.graph.data.ValueWrapper val = + (com.vesoft.nebula.driver.graph.data.ValueWrapper) entry.getValue(); + kvs.put(key.toString(), new ValueWrapper(val, timezoneOffset)); + } + return kvs; + } + throw new InvalidValueException( + "Cannot get field `map' because value's type is " + value.getDataTypeString()); + } + + public TimeWrapper asTime() throws InvalidValueException { + TimeWrapper wrapper; + if (value.isLocalTime()) { + wrapper = new TimeWrapper(value.asLocalTime()); + } else if (value.isZonedTime()) { + wrapper = new TimeWrapper(value.asZonedTime()); + } else { + throw new InvalidValueException( + "Cannot get field time because value's type is " + value.getDataTypeString()); + } + return (TimeWrapper) wrapper.setTimezoneOffset(timezoneOffset); + } + + public DateWrapper asDate() throws InvalidValueException { + if (value.isDate()) { + return (DateWrapper) new DateWrapper(value.asDate()).setTimezoneOffset(timezoneOffset); + } + throw new InvalidValueException( + "Cannot get field date because value's type is " + value.getDataTypeString()); + } + + public DateTimeWrapper asDateTime() throws InvalidValueException { + DateTimeWrapper wrapper; + if (value.isLocalDateTime()) { + wrapper = new DateTimeWrapper(value.asLocalDateTime()); + } else if (value.isZonedDateTime()) { + wrapper = new DateTimeWrapper(value.asZonedDateTime()); + } else { + throw new InvalidValueException( + "Cannot get field datetime because value's type is " + value.getDataTypeString()); + } + return (DateTimeWrapper) wrapper.setTimezoneOffset(timezoneOffset); + } + + public Node asNode() throws InvalidValueException, UnsupportedEncodingException { + if (value.isNode()) { + return (Node) new Node(value.asNode()).setTimezoneOffset(timezoneOffset); + } + throw new InvalidValueException( + "Cannot get field Node because value's type is " + value.getDataTypeString()); + } + + public Relationship asRelationship() throws InvalidValueException { + if (value.isEdge()) { + return (Relationship) new Relationship(value.asEdge()).setTimezoneOffset( + timezoneOffset); + } + throw new InvalidValueException( + "Cannot get field Relationship because value's type is " + value.getDataTypeString()); + } + + public PathWrapper asPath() throws InvalidValueException, UnsupportedEncodingException { + if (value.isPath()) { + return new PathWrapper(value.asPath(), timezoneOffset); + } + throw new InvalidValueException( + "Cannot get field PathWrapper because value's type is " + value.getDataTypeString()); + } + + public GeographyWrapper asGeography() throws InvalidValueException { + if (value.isGeography()) { + return (GeographyWrapper) new GeographyWrapper(value.asGeography()) + .setTimezoneOffset(timezoneOffset); + } + throw new InvalidValueException( + "Cannot get field GeographyWrapper because value's type is " + value.getDataTypeString()); + } + + public DurationWrapper asDuration() throws InvalidValueException { + if (value.isDuration()) { + return (DurationWrapper) new DurationWrapper(value.asDuration()) + .setTimezoneOffset(timezoneOffset); + } + throw new InvalidValueException( + "Cannot get field DurationWrapper because value's type is " + value.getDataTypeString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ValueWrapper that = (ValueWrapper) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + try { + if (isNull()) { + return asNull().toString(); + } else if (isBoolean()) { + return String.valueOf(asBoolean()); + } else if (isLong()) { + return String.valueOf(asLong()); + } else if (isDouble()) { + return String.valueOf(asDouble()); + } else if (isString()) { + return "\"" + asString() + "\""; + } else if (isList()) { + return asList().toString(); + } else if (isSet()) { + return asSet().toString(); + } else if (isMap()) { + return asMap().toString(); + } else if (isTime()) { + return asTime().toString(); + } else if (isDate()) { + return asDate().toString(); + } else if (isDateTime()) { + return asDateTime().toString(); + } else if (isVertex()) { + return asNode().toString(); + } else if (isEdge()) { + return asRelationship().toString(); + } else if (isPath()) { + return asPath().toString(); + } else if (isGeography()) { + return asGeography().toString(); + } else if (isDuration()) { + return asDuration().toString(); + } + return "Unknown type: " + value.getDataTypeString(); + } catch (Exception e) { + return e.getMessage(); + } + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/AuthFailedException.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/AuthFailedException.java new file mode 100644 index 000000000..43a47afc9 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/AuthFailedException.java @@ -0,0 +1,15 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.exception; + +/** + * Thrown when user authentication against the NebulaGraph server fails. + */ +public class AuthFailedException extends Exception { + public AuthFailedException(String message) { + super(String.format("Auth failed: %s", message)); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/BindSpaceFailedException.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/BindSpaceFailedException.java new file mode 100644 index 000000000..724145324 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/BindSpaceFailedException.java @@ -0,0 +1,17 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.exception; + +/** + * Thrown when binding the working graph (space) fails. + */ +public class BindSpaceFailedException extends Exception { + private static final long serialVersionUID = -8678623814979666625L; + + public BindSpaceFailedException(String message) { + super(String.format("use space failed: %s", message)); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/ClientServerIncompatibleException.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/ClientServerIncompatibleException.java new file mode 100644 index 000000000..a460c238e --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/ClientServerIncompatibleException.java @@ -0,0 +1,16 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.exception; + +/** + * Thrown when the client and the remote server versions are incompatible. + */ +public class ClientServerIncompatibleException extends Exception { + public ClientServerIncompatibleException(String message) { + super("Current client is not compatible with the remote server, please check the " + + "version: " + message); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/IOErrorException.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/IOErrorException.java new file mode 100644 index 000000000..238ce1167 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/IOErrorException.java @@ -0,0 +1,34 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.exception; + +/** + * Thrown when an IO error occurs while talking to the NebulaGraph server. + * + *

The {@code type} field mirrors the v3 client's error-type constants. + */ +public class IOErrorException extends java.lang.Exception { + public static final int E_UNKNOWN = 0; + + public static final int E_ALL_BROKEN = 1; + + public static final int E_CONNECT_BROKEN = 2; + + public static final int E_TIME_OUT = 4; + + public static final int E_NO_OPEN = 5; + + private int type = E_UNKNOWN; + + public IOErrorException(int errorType, String message) { + super(message); + this.type = errorType; + } + + public int getType() { + return type; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/InvalidConfigException.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/InvalidConfigException.java new file mode 100644 index 000000000..c604f4caa --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/InvalidConfigException.java @@ -0,0 +1,15 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.exception; + +/** + * Thrown when an illegal configuration is detected. + */ +public class InvalidConfigException extends RuntimeException { + public InvalidConfigException(String message) { + super(message); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/InvalidSessionException.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/InvalidSessionException.java new file mode 100644 index 000000000..fed70c69d --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/InvalidSessionException.java @@ -0,0 +1,15 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.exception; + +/** + * Thrown when a released or invalidated session is used. + */ +public class InvalidSessionException extends RuntimeException { + public InvalidSessionException() { + super("The session was released, could not use again."); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/InvalidValueException.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/InvalidValueException.java new file mode 100644 index 000000000..70e146451 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/InvalidValueException.java @@ -0,0 +1,15 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.exception; + +/** + * Thrown when a value is converted to an incompatible type. + */ +public class InvalidValueException extends RuntimeException { + public InvalidValueException(String message) { + super(message); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/NotValidConnectionException.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/NotValidConnectionException.java new file mode 100644 index 000000000..48419e7f6 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/exception/NotValidConnectionException.java @@ -0,0 +1,15 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.exception; + +/** + * Thrown when a connection cannot be obtained from the pool. + */ +public class NotValidConnectionException extends Exception { + public NotValidConnectionException(String message) { + super(String.format("No extra connection: %s", message)); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/AuthResult.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/AuthResult.java new file mode 100644 index 000000000..1a1f229cb --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/AuthResult.java @@ -0,0 +1,35 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import java.io.Serializable; + +/** + * Authentication result, matching the v3 client. + * + *

The v5 driver has no per-session timezone offset, so {@link #getTimezoneOffset()} always + * returns {@code 0}. + */ +public class AuthResult implements Serializable { + + private static final long serialVersionUID = 8795815613377375650L; + + private final long sessionId; + private final int timezoneOffset; + + public AuthResult(long sessionId, int timezoneOffset) { + this.sessionId = sessionId; + this.timezoneOffset = timezoneOffset; + } + + public long getSessionId() { + return sessionId; + } + + public int getTimezoneOffset() { + return timezoneOffset; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/ConnObjectPool.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/ConnObjectPool.java new file mode 100644 index 000000000..d782b1638 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/ConnObjectPool.java @@ -0,0 +1,69 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import com.vesoft.nebula.driver.v3client.graph.NebulaPoolConfig; +import java.io.Serializable; +import org.apache.commons.pool2.BasePooledObjectFactory; +import org.apache.commons.pool2.PooledObject; +import org.apache.commons.pool2.impl.DefaultPooledObject; + +/** + * Source-compatibility shim for the v3 client's connection object pool. + * + *

The v5 driver manages its own client pool; this factory is not used by + * {@link NebulaPool#getSession} and {@link #create()} is unsupported. + */ +public class ConnObjectPool extends BasePooledObjectFactory + implements Serializable { + + private static final long serialVersionUID = 6101157301791971560L; + + private final NebulaPoolConfig config; + private final LoadBalancer loadBalancer; + + public ConnObjectPool(LoadBalancer loadBalancer, NebulaPoolConfig config) { + this.loadBalancer = loadBalancer; + this.config = config; + } + + @Override + public SyncConnection create() throws Exception { + throw new UnsupportedOperationException( + "The v3 connection object pool is not used by the v5 driver."); + } + + @Override + public PooledObject wrap(SyncConnection connection) { + return new DefaultPooledObject<>(connection); + } + + @Override + public void destroyObject(PooledObject p) throws Exception { + p.getObject().close(); + super.destroyObject(p); + } + + @Override + public boolean validateObject(PooledObject p) { + return p.getObject() != null; + } + + @SuppressWarnings("unused") + public boolean init() { + return loadBalancer.isServersOK(); + } + + @SuppressWarnings("unused") + public void updateServerStatus() { + loadBalancer.updateServersStatus(); + } + + @SuppressWarnings("unused") + public NebulaPoolConfig getConfig() { + return config; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/Connection.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/Connection.java new file mode 100644 index 000000000..6c946d125 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/Connection.java @@ -0,0 +1,50 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.SSLParam; +import com.vesoft.nebula.driver.v3client.graph.exception.ClientServerIncompatibleException; +import com.vesoft.nebula.driver.v3client.graph.exception.IOErrorException; +import java.io.Serializable; +import java.util.Map; + +/** + * Abstract connection, matching the v3 client. The v5 driver manages connections internally via + * {@code GrpcConnection}, so this type exists for source compatibility only. + */ +public abstract class Connection implements Serializable { + + private static final long serialVersionUID = -8425216612015802331L; + + protected HostAddress serverAddr = null; + + public HostAddress getServerAddress() { + return this.serverAddr; + } + + public abstract void open(HostAddress address, int timeout, SSLParam sslParam) + throws IOErrorException, ClientServerIncompatibleException; + + public abstract void open(HostAddress address, int timeout, + SSLParam sslParam, boolean isUseHttp2, Map headers) + throws IOErrorException, ClientServerIncompatibleException; + + public abstract void open(HostAddress address, int timeout) + throws IOErrorException, ClientServerIncompatibleException; + + public abstract void open(HostAddress address, int timeout, + boolean isUseHttp2, Map headers) + throws IOErrorException, ClientServerIncompatibleException; + + public abstract void reopen() throws IOErrorException, ClientServerIncompatibleException; + + public abstract void close(); + + public abstract boolean ping(); + + public abstract boolean ping(long sessionID); +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/LoadBalancer.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/LoadBalancer.java new file mode 100644 index 000000000..2f7c9d07f --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/LoadBalancer.java @@ -0,0 +1,21 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; + +/** + * Server load-balancer interface, matching the v3 client. + */ +public interface LoadBalancer { + HostAddress getAddress(); + + void close(); + + void updateServersStatus(); + + boolean isServersOK(); +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/NebulaPool.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/NebulaPool.java new file mode 100644 index 000000000..9d8a87e1b --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/NebulaPool.java @@ -0,0 +1,269 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import com.vesoft.nebula.driver.v3client.graph.NebulaPoolConfig; +import com.vesoft.nebula.driver.v3client.graph.data.CASignedSSLParam; +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.SSLParam; +import com.vesoft.nebula.driver.v3client.graph.data.SelfSignedSSLParam; +import com.vesoft.nebula.driver.v3client.graph.exception.AuthFailedException; +import com.vesoft.nebula.driver.v3client.graph.exception.ClientServerIncompatibleException; +import com.vesoft.nebula.driver.v3client.graph.exception.IOErrorException; +import com.vesoft.nebula.driver.v3client.graph.exception.InvalidConfigException; +import com.vesoft.nebula.driver.v3client.graph.exception.NotValidConnectionException; +import java.io.Serializable; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A pool of connections/sessions, matching the v3 client's {@code NebulaPool} API. + * + *

The v3 pool is credential-less at init time and authenticates per {@link #getSession}; the v5 + * driver bakes credentials into its client pool. This adapter therefore lazily builds one v5 pool + * per distinct username on the first {@code getSession} call. + */ +public class NebulaPool implements Serializable { + + private static final long serialVersionUID = 6226487268001127885L; + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private List addresses; + private NebulaPoolConfig config; + + private final Map pools = + new HashMap<>(); + + private final AtomicBoolean hasInit = new AtomicBoolean(false); + private final AtomicBoolean isClosed = new AtomicBoolean(false); + + private void checkConfig(NebulaPoolConfig config) { + if (config.getIdleTime() < 0) { + throw new InvalidConfigException( + "Config idleTime:" + config.getIdleTime() + " is illegal"); + } + if (config.getMaxConnSize() <= 0) { + throw new InvalidConfigException( + "Config maxConnSize:" + config.getMaxConnSize() + " is illegal"); + } + if (config.getMinConnSize() < 0 || config.getMinConnSize() > config.getMaxConnSize()) { + throw new InvalidConfigException( + "Config minConnSize:" + config.getMinConnSize() + " is illegal"); + } + if (config.getTimeout() < 0) { + throw new InvalidConfigException( + "Config timeout:" + config.getTimeout() + " is illegal"); + } + if (config.getWaitTime() < 0) { + throw new InvalidConfigException( + "Config waitTime:" + config.getWaitTime() + " is illegal"); + } + if (config.getMinClusterHealthRate() < 0) { + throw new InvalidConfigException( + "Config minClusterHealthRate:" + config.getMinClusterHealthRate() + " is illegal"); + } + } + + /** + * @param addresses the graphd services addresses + * @param config the config for the pool + * @return {@code true} if the config is valid. The v5 driver performs the actual server + * health check lazily when the first session is requested. + * @throws UnknownHostException if host address is illegal + * @throws InvalidConfigException if config is illegal + */ + public boolean init(List addresses, NebulaPoolConfig config) + throws UnknownHostException, InvalidConfigException { + checkInit(); + checkConfig(config); + this.addresses = new ArrayList<>(addresses); + this.config = config; + hasInit.set(true); + return true; + } + + /** + * close the pool, all connections will be closed. + */ + public void close() { + if (isClosed.compareAndSet(false, true)) { + synchronized (pools) { + for (com.vesoft.nebula.driver.graph.net.NebulaPool pool : pools.values()) { + pool.close(); + } + pools.clear(); + } + } + } + + /** + * get a session from the pool. + * + * @param userName the userName to authenticate with graphd + * @param password the password to authenticate with graphd + * @param reconnect whether to retry after the connection is disconnected + * @return Session + * @throws NotValidConnectionException if get connection failed + * @throws IOErrorException if an IO error occurs + * @throws AuthFailedException if authentication failed + */ + public Session getSession(String userName, String password, boolean reconnect) + throws NotValidConnectionException, IOErrorException, AuthFailedException, + ClientServerIncompatibleException { + checkNoInitAndClosed(); + com.vesoft.nebula.driver.graph.net.NebulaPool pool = getOrCreatePool(userName, password); + com.vesoft.nebula.driver.graph.net.NebulaClient client; + try { + client = pool.getClient(); + } catch (Exception e) { + throw new NotValidConnectionException(e.getMessage()); + } + return new Session(client, pool, reconnect); + } + + public int getActiveConnNum() { + checkNoInitAndClosed(); + int total = 0; + synchronized (pools) { + for (com.vesoft.nebula.driver.graph.net.NebulaPool pool : pools.values()) { + total += pool.getActiveSessions(); + } + } + return total; + } + + public int getIdleConnNum() { + checkNoInitAndClosed(); + int total = 0; + synchronized (pools) { + for (com.vesoft.nebula.driver.graph.net.NebulaPool pool : pools.values()) { + total += pool.getIdleSessions(); + } + } + return total; + } + + public int getWaitersNum() { + checkNoInitAndClosed(); + int total = 0; + synchronized (pools) { + for (com.vesoft.nebula.driver.graph.net.NebulaPool pool : pools.values()) { + total += pool.getWaiters(); + } + } + return total; + } + + private com.vesoft.nebula.driver.graph.net.NebulaPool getOrCreatePool(String user, + String password) + throws AuthFailedException, IOErrorException { + synchronized (pools) { + com.vesoft.nebula.driver.graph.net.NebulaPool pool = pools.get(user); + if (pool == null) { + pool = buildPool(user, password); + pools.put(user, pool); + } + return pool; + } + } + + private com.vesoft.nebula.driver.graph.net.NebulaPool buildPool(String user, String password) + throws AuthFailedException, IOErrorException { + StringBuilder sb = new StringBuilder(); + for (HostAddress address : addresses) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(address.toString()); + } + com.vesoft.nebula.driver.graph.net.NebulaPool.Builder builder = + com.vesoft.nebula.driver.graph.net.NebulaPool.builder(sb.toString(), user, password); + builder.withMinClientSize(config.getMinConnSize()); + builder.withMaxClientSize(config.getMaxConnSize()); + if (config.getTimeout() > 0) { + builder.withConnectTimeoutMills(config.getTimeout()); + builder.withRequestTimeoutMills(config.getTimeout()); + } + if (config.getIdleTime() > 0) { + builder.withMinEvictableIdleTimeMillis(config.getIdleTime()); + } + if (config.getIntervalIdle() > 0) { + builder.withIdleEvictScheduleMills(config.getIntervalIdle()); + } + if (config.getWaitTime() > 0) { + builder.withMaxWaitMills(config.getWaitTime()); + } + builder.withStrictlyServerHealthy(config.getMinClusterHealthRate() >= 1.0); + applyTls(builder, config); + try { + return builder.build(); + } catch (com.vesoft.nebula.driver.graph.exception.AuthFailedException e) { + throw Session.toCompatAuth(e); + } catch (com.vesoft.nebula.driver.graph.exception.IOErrorException e) { + throw Session.toCompat(e); + } + } + + private void applyTls(com.vesoft.nebula.driver.graph.net.NebulaPool.Builder builder, + NebulaPoolConfig config) { + if (!config.isEnableSsl()) { + return; + } + builder.withEnableTls(true); + SSLParam ssl = config.getSslParam(); + if (ssl == null) { + builder.withDisableVerifyServerCert(true); + return; + } + if (ssl.isSkipVerifyServer()) { + builder.withDisableVerifyServerCert(true); + } + if (ssl instanceof CASignedSSLParam) { + CASignedSSLParam ca = (CASignedSSLParam) ssl; + if (ca.getCaCrtFilePath() != null) { + builder.withTlsCa(ca.getCaCrtFilePath()); + } + if (ca.getCrtFilePath() != null && ca.getKeyFilePath() != null) { + builder.withTlsCert(ca.getCrtFilePath(), ca.getKeyFilePath()); + } + } else if (ssl instanceof SelfSignedSSLParam) { + SelfSignedSSLParam self = (SelfSignedSSLParam) ssl; + builder.withDisableVerifyServerCert(true); + if (self.getCrtFilePath() != null && self.getKeyFilePath() != null) { + builder.withTlsCert(self.getCrtFilePath(), self.getKeyFilePath()); + } + } + } + + private void checkNoInit() { + if (!hasInit.get()) { + throw new RuntimeException( + "The pool has not been initialized, please initialize it first."); + } + } + + private void checkInit() { + if (hasInit.get()) { + throw new RuntimeException( + "The pool has already been initialized. " + + "Please do not initialize the pool repeatedly."); + } + } + + private void checkNoInitAndClosed() { + checkNoInit(); + if (isClosed.get()) { + throw new RuntimeException("The pool has closed. Couldn't use again."); + } + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/RoundRobinLoadBalancer.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/RoundRobinLoadBalancer.java new file mode 100644 index 000000000..aa6affec9 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/RoundRobinLoadBalancer.java @@ -0,0 +1,80 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.SSLParam; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Source-compatibility shim for the v3 client's round-robin load balancer. + * + *

The v5 driver performs its own server health management, so this shim only provides a + * round-robin {@link #getAddress()} over the configured addresses. + */ +public class RoundRobinLoadBalancer implements LoadBalancer { + + private final List addresses = new ArrayList<>(); + private final AtomicInteger pos = new AtomicInteger(0); + + public RoundRobinLoadBalancer(List addresses, int timeout, + double minClusterHealthRate) { + this(addresses); + } + + public RoundRobinLoadBalancer(List addresses, int timeout, + double minClusterHealthRate, boolean useHttp2, + Map headers) { + this(addresses); + } + + public RoundRobinLoadBalancer(List addresses, int timeout, SSLParam sslParam, + double minClusterHealthRate) { + this(addresses); + } + + public RoundRobinLoadBalancer(List addresses, int timeout, SSLParam sslParam, + double minClusterHealthRate, boolean useHttp2, + Map headers) { + this(addresses); + } + + private RoundRobinLoadBalancer(List addresses) { + if (addresses != null) { + this.addresses.addAll(addresses); + } + } + + @Override + public HostAddress getAddress() { + if (addresses.isEmpty()) { + return null; + } + return addresses.get(Math.abs(pos.getAndIncrement()) % addresses.size()); + } + + @Override + public void close() { + // no-op + } + + @Override + public void updateServersStatus() { + // no-op; health is managed by the v5 driver. + } + + @Override + public boolean isServersOK() { + return !addresses.isEmpty(); + } + + @SuppressWarnings("unused") + private static final Map EMPTY = new HashMap<>(); +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/Session.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/Session.java new file mode 100644 index 000000000..d60e9a6bf --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/Session.java @@ -0,0 +1,372 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import com.vesoft.nebula.driver.v3client.graph.data.ValueWrapper; +import com.vesoft.nebula.driver.v3client.graph.exception.AuthFailedException; +import com.vesoft.nebula.driver.v3client.graph.exception.IOErrorException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A session against NebulaGraph, matching the v3 client's {@code Session} API. + * + *

Internally it delegates to a v5 driver {@link com.vesoft.nebula.driver.graph.net.NebulaClient}; + * one v5 client is one server-side session. + */ +public class Session implements Serializable, AutoCloseable { + + private static final long serialVersionUID = -8855886967097862376L; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final long sessionID; + private final int timezoneOffset; + private final com.vesoft.nebula.driver.graph.net.NebulaPool pool; + private final boolean retryConnect; + private final AtomicBoolean released = new AtomicBoolean(false); + private volatile com.vesoft.nebula.driver.graph.net.NebulaClient client; + + public Session(com.vesoft.nebula.driver.graph.net.NebulaClient client, + com.vesoft.nebula.driver.graph.net.NebulaPool pool, + boolean retryConnect) { + this.client = client; + this.sessionID = client.getSessionId(); + this.timezoneOffset = 0; + this.pool = pool; + this.retryConnect = retryConnect; + } + + public synchronized ResultSet execute(String stmt) throws IOErrorException { + return executeWithParameter(stmt, Collections.emptyMap()); + } + + public synchronized ResultSet executeWithParameter(String stmt, Map parameterMap) + throws IOErrorException { + checkReleased(); + String gql = inlineParameters(stmt, parameterMap); + try { + return new ResultSet(client.execute(gql), timezoneOffset); + } catch (com.vesoft.nebula.driver.graph.exception.IOErrorException e) { + if (isConnectBroken(e) && retryConnect && reconnect()) { + try { + return new ResultSet(client.execute(gql), timezoneOffset); + } catch (com.vesoft.nebula.driver.graph.exception.IOErrorException e2) { + throw toCompat(e2); + } + } + throw toCompat(e); + } + } + + public ResultSet executeWithTimeout(String stmt, long timeoutMs) throws IOErrorException { + return executeWithParameterTimeout(stmt, Collections.emptyMap(), timeoutMs); + } + + public synchronized ResultSet executeWithParameterTimeout(String stmt, + Map parameterMap, + long timeoutMs) + throws IOErrorException { + checkReleased(); + if (timeoutMs <= 0) { + throw new IllegalArgumentException("timeout should be a positive number"); + } + String gql = inlineParameters(stmt, parameterMap); + try { + return new ResultSet(client.execute(gql, timeoutMs), timezoneOffset); + } catch (com.vesoft.nebula.driver.graph.exception.IOErrorException e) { + if (isConnectBroken(e) && retryConnect && reconnect()) { + try { + return new ResultSet(client.execute(gql, timeoutMs), timezoneOffset); + } catch (com.vesoft.nebula.driver.graph.exception.IOErrorException e2) { + throw toCompat(e2); + } + } + throw toCompat(e); + } + } + + public synchronized String executeJson(String stmt) throws IOErrorException { + return executeJsonWithParameter(stmt, Collections.emptyMap()); + } + + public synchronized String executeJsonWithParameter(String stmt, + Map parameterMap) + throws IOErrorException { + ResultSet resultSet = executeWithParameter(stmt, parameterMap); + return toJson(resultSet); + } + + public synchronized boolean ping() { + if (client == null) { + return false; + } + return client.ping(); + } + + public synchronized boolean pingSession() { + if (client == null) { + return false; + } + return client.ping(); + } + + public synchronized void release() { + if (client == null) { + return; + } + if (released.compareAndSet(false, true)) { + if (pool != null) { + pool.returnClient(client); + } else { + client.close(); + } + client = null; + } + } + + public synchronized HostAddress getGraphHost() { + if (client == null) { + return null; + } + return parseHost(client.getHost()); + } + + public long getSessionID() { + return sessionID; + } + + @Override + public synchronized void close() { + release(); + } + + private void checkReleased() throws IOErrorException { + if (client == null) { + throw new IOErrorException(IOErrorException.E_CONNECT_BROKEN, + "The session was released, couldn't use again."); + } + } + + private boolean reconnect() { + if (pool == null) { + return false; + } + client.close(); + pool.returnClient(client); + try { + client = pool.getClient(); + return true; + } catch (Exception e) { + log.error("Reconnect failed: " + e); + return false; + } + } + + public static AuthFailedException toCompatAuth( + com.vesoft.nebula.driver.graph.exception.AuthFailedException e) { + String msg = e.getMessage(); + String prefix = "Auth failed: "; + if (msg != null && msg.startsWith(prefix)) { + msg = msg.substring(prefix.length()); + } + return new AuthFailedException(msg); + } + + public static boolean isConnectBroken( + com.vesoft.nebula.driver.graph.exception.IOErrorException e) { + return e.getType() == com.vesoft.nebula.driver.graph.exception.IOErrorException + .E_CONNECT_BROKEN; + } + + public static IOErrorException toCompat( + com.vesoft.nebula.driver.graph.exception.IOErrorException e) { + int type; + switch (e.getType()) { + case com.vesoft.nebula.driver.graph.exception.IOErrorException.E_CONNECT_BROKEN: + type = IOErrorException.E_CONNECT_BROKEN; + break; + case com.vesoft.nebula.driver.graph.exception.IOErrorException.E_ALL_BROKEN: + type = IOErrorException.E_ALL_BROKEN; + break; + case com.vesoft.nebula.driver.graph.exception.IOErrorException.E_TIME_OUT: + type = IOErrorException.E_TIME_OUT; + break; + case com.vesoft.nebula.driver.graph.exception.IOErrorException.E_NO_OPEN: + type = IOErrorException.E_NO_OPEN; + break; + default: + type = IOErrorException.E_UNKNOWN; + } + return new IOErrorException(type, e.getMessage()); + } + + public static String toJson(ResultSet resultSet) { + JSONObject root = new JSONObject(); + JSONArray errors = new JSONArray(); + JSONObject error = new JSONObject(); + error.put("code", resultSet.getErrorCode()); + error.put("message", resultSet.getErrorMessage()); + errors.add(error); + root.put("errors", errors); + + JSONArray results = new JSONArray(); + JSONObject result = new JSONObject(); + result.put("columns", resultSet.getColumnNames()); + result.put("latencyInUs", resultSet.getLatency()); + result.put("spaceName", resultSet.getSpaceName()); + result.put("comment", resultSet.getComment()); + JSONArray data = new JSONArray(); + for (int i = 0; i < resultSet.rowsSize(); i++) { + JSONObject rowObj = new JSONObject(); + JSONArray row = new JSONArray(); + for (ValueWrapper value : resultSet.rowValues(i)) { + row.add(unquote(value)); + } + rowObj.put("row", row); + rowObj.put("meta", JSON.parseObject("{}")); + data.add(rowObj); + } + result.put("data", data); + results.add(result); + root.put("results", results); + return JSON.toJSONString(root); + } + + private static Object unquote(ValueWrapper value) { + String str = value.toString(); + if (str != null && str.length() >= 2 && str.startsWith("\"") && str.endsWith("\"")) { + return str.substring(1, str.length() - 1); + } + return str; + } + + /** + * Inline {@code $param} placeholders with GQL literals (the v5 execute RPC takes no parameter + * map). Keys are replaced longest-first to avoid prefix collisions. + */ + public static String inlineParameters(String stmt, Map parameterMap) { + if (parameterMap == null || parameterMap.isEmpty()) { + return stmt; + } + String gql = stmt; + List keys = new ArrayList<>(parameterMap.keySet()); + keys.sort((a, b) -> Integer.compare(b.length(), a.length())); + for (String key : keys) { + gql = gql.replace("$" + key, value2GqlLiteral(parameterMap.get(key))); + } + return gql; + } + + /** + * Convert a Java value to a GQL literal. Supports the v3 client's parameter value types: + * null, boolean, numeric, string, bytes, list/collection, map, and falls back to a quoted + * string for other types. + */ + public static String value2GqlLiteral(Object value) { + if (value == null) { + return "NULL"; + } + if (value instanceof Boolean + || value instanceof Integer + || value instanceof Short + || value instanceof Byte + || value instanceof Long + || value instanceof Float + || value instanceof Double) { + return String.valueOf(value); + } + if (value instanceof String) { + return "\"" + escape((String) value) + "\""; + } + if (value instanceof byte[]) { + return "\"" + escape(new String((byte[]) value, StandardCharsets.UTF_8)) + "\""; + } + if (value instanceof Character) { + return "\"" + escape(value.toString()) + "\""; + } + if (value instanceof Collection) { + List literals = new ArrayList<>(); + for (Object element : (Collection) value) { + literals.add(value2GqlLiteral(element)); + } + return "[" + String.join(", ", literals) + "]"; + } + if (value instanceof Map) { + List entries = new ArrayList<>(); + for (Map.Entry entry : ((Map) value).entrySet()) { + entries.add(entry.getKey().toString() + ": " + value2GqlLiteral(entry.getValue())); + } + return "{" + String.join(", ", entries) + "}"; + } + return "\"" + escape(value.toString()) + "\""; + } + + private static String escape(String value) { + StringBuilder builder = new StringBuilder(); + for (char c : value.toCharArray()) { + switch (c) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\t': + builder.append("\\t"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\b': + builder.append("\\b"); + break; + case '\'': + builder.append("\\'"); + break; + default: + builder.append(c); + break; + } + } + return builder.toString(); + } + + public static HostAddress parseHost(String host) { + if (host == null || host.isEmpty()) { + return null; + } + if (host.startsWith("[")) { + int close = host.indexOf(']'); + if (close < 0) { + return new HostAddress(host, 0); + } + return new HostAddress(host.substring(1, close), + Integer.parseInt(host.substring(close + 2))); + } + int idx = host.lastIndexOf(':'); + if (idx < 0) { + return new HostAddress(host, 0); + } + return new HostAddress(host.substring(0, idx), Integer.parseInt(host.substring(idx + 1))); + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SessionState.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SessionState.java new file mode 100644 index 000000000..08234fcbd --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SessionState.java @@ -0,0 +1,13 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +/** + * The lifecycle state of a pooled session, matching the v3 client. + */ +public enum SessionState { + IDLE, USED +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SessionWrapper.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SessionWrapper.java new file mode 100644 index 000000000..32a3814cc --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SessionWrapper.java @@ -0,0 +1,62 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import com.vesoft.nebula.driver.v3client.graph.exception.IOErrorException; +import com.vesoft.nebula.driver.v3client.graph.exception.InvalidSessionException; +import java.io.Serializable; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A single-use wrapper around a {@link Session}, matching the v3 client. + */ +public class SessionWrapper implements Serializable { + + private static final long serialVersionUID = -8128331485649098264L; + + private final Session session; + private final long sessionID; + private final AtomicBoolean available = new AtomicBoolean(true); + + public SessionWrapper(Session session) { + this.session = session; + this.sessionID = session.getSessionID(); + } + + public ResultSet execute(String stmt) throws IOErrorException { + if (!available()) { + throw new InvalidSessionException(); + } + return session.execute(stmt); + } + + public boolean ping() { + return session.pingSession(); + } + + void setNoAvailable() { + this.available.set(false); + } + + boolean available() { + return available.get(); + } + + void release() { + session.release(); + setNoAvailable(); + } + + Session getSession() { + return session; + } + + @SuppressWarnings("unused") + public long getSessionID() { + return sessionID; + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SessionsManager.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SessionsManager.java new file mode 100644 index 000000000..8a53399b4 --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SessionsManager.java @@ -0,0 +1,127 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import com.vesoft.nebula.driver.v3client.graph.SessionsManagerConfig; +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import com.vesoft.nebula.driver.v3client.graph.exception.AuthFailedException; +import com.vesoft.nebula.driver.v3client.graph.exception.ClientServerIncompatibleException; +import com.vesoft.nebula.driver.v3client.graph.exception.IOErrorException; +import com.vesoft.nebula.driver.v3client.graph.exception.NotValidConnectionException; +import java.io.Serializable; +import java.net.UnknownHostException; +import java.util.BitSet; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Manages a set of single-use sessions, matching the v3 client's {@code SessionsManager}. + */ +public class SessionsManager implements Serializable { + + private static final long serialVersionUID = 7519424097351713021L; + + private final SessionsManagerConfig config; + private NebulaPool pool = null; + private final CopyOnWriteArrayList sessionList; + private BitSet canUseBitSet; + private Boolean isClose = false; + private Boolean isInited = false; + + public SessionsManager(SessionsManagerConfig config) { + this.config = config; + this.sessionList = new CopyOnWriteArrayList<>(); + checkConfig(); + } + + private void checkConfig() { + if (config.getAddresses().isEmpty()) { + throw new RuntimeException("Empty graph addresses"); + } + if (config.getSpaceName().isEmpty()) { + throw new RuntimeException("Empty space name"); + } + } + + public synchronized SessionWrapper getSessionWrapper() + throws RuntimeException, ClientServerIncompatibleException { + checkClose(); + if (!isInited) { + init(); + } + if (canUseBitSet.isEmpty() + && sessionList.size() >= config.getPoolConfig().getMaxConnSize()) { + throw new RuntimeException("The SessionsManager does not have available sessions."); + } + if (!canUseBitSet.isEmpty()) { + int index = canUseBitSet.nextSetBit(0); + if (index >= 0) { + if (canUseBitSet.get(index)) { + canUseBitSet.set(index, false); + return sessionList.get(index); + } + } + } + try { + Session session = pool.getSession( + config.getUserName(), config.getPassword(), config.getReconnect()); + ResultSet resultSet = session.execute( + String.format("SESSION SET GRAPH \"%s\"", config.getSpaceName())); + if (!resultSet.isSucceeded()) { + throw new RuntimeException( + "Switch graph `" + config.getSpaceName() + "' failed: " + + resultSet.getErrorMessage()); + } + SessionWrapper sessionWrapper = new SessionWrapper(session); + sessionList.add(sessionWrapper); + return sessionWrapper; + } catch (AuthFailedException | NotValidConnectionException | IOErrorException e) { + throw new RuntimeException("Get session failed: " + e.getMessage()); + } + } + + public synchronized void returnSessionWrapper(SessionWrapper session) { + checkClose(); + if (session == null) { + return; + } + int index = sessionList.indexOf(session); + if (index >= 0) { + Session ses = session.getSession(); + sessionList.set(index, new SessionWrapper(ses)); + session.setNoAvailable(); + canUseBitSet.set(index, true); + } + } + + public synchronized void close() { + for (SessionWrapper session : sessionList) { + session.release(); + } + pool.close(); + sessionList.clear(); + isClose = true; + } + + private void init() throws RuntimeException { + try { + pool = new NebulaPool(); + if (!pool.init(config.getAddresses(), config.getPoolConfig())) { + throw new RuntimeException("Init pool failed: services are broken."); + } + canUseBitSet = new BitSet(config.getPoolConfig().getMaxConnSize()); + canUseBitSet.set(0, config.getPoolConfig().getMaxConnSize(), false); + } catch (UnknownHostException e) { + throw new RuntimeException("Init the pool failed: " + e.getMessage()); + } + isInited = true; + } + + private void checkClose() { + if (isClose) { + throw new RuntimeException("The SessionsManager was closed."); + } + } +} diff --git a/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SyncConnection.java b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SyncConnection.java new file mode 100644 index 000000000..1b4bf803d --- /dev/null +++ b/client-v3compat/src/main/java/com/vesoft/nebula/driver/v3client/graph/net/SyncConnection.java @@ -0,0 +1,90 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.SSLParam; +import com.vesoft.nebula.driver.v3client.graph.exception.AuthFailedException; +import com.vesoft.nebula.driver.v3client.graph.exception.ClientServerIncompatibleException; +import com.vesoft.nebula.driver.v3client.graph.exception.IOErrorException; +import java.util.Map; + +/** + * Source-compatibility shim for the v3 client's {@code SyncConnection}. + * + *

The v5 driver talks gRPC and manages connections internally; applications should obtain + * sessions through {@link NebulaPool#getSession}. Direct use of the connection-level API is not + * supported and the authenticate/execute methods throw {@link UnsupportedOperationException}. + */ +public class SyncConnection extends Connection { + + private SSLParam sslParam = null; + private int timeout = 0; + private boolean useHttp2 = false; + private Map headers = null; + private boolean opened = false; + + @Override + public void open(HostAddress address, int timeout, SSLParam sslParam) + throws IOErrorException, ClientServerIncompatibleException { + open(address, timeout, sslParam, false, null); + } + + @Override + public void open(HostAddress address, int timeout, SSLParam sslParam, boolean isUseHttp2, + Map headers) + throws IOErrorException, ClientServerIncompatibleException { + this.serverAddr = address; + this.timeout = timeout; + this.sslParam = sslParam; + this.useHttp2 = isUseHttp2; + this.headers = headers; + this.opened = true; + } + + @Override + public void open(HostAddress address, int timeout) + throws IOErrorException, ClientServerIncompatibleException { + open(address, timeout, null, false, null); + } + + @Override + public void open(HostAddress address, int timeout, boolean isUseHttp2, + Map headers) + throws IOErrorException, ClientServerIncompatibleException { + open(address, timeout, null, isUseHttp2, headers); + } + + @Override + public void reopen() throws IOErrorException, ClientServerIncompatibleException { + close(); + if (serverAddr != null) { + open(serverAddr, timeout, sslParam, useHttp2, headers); + } + } + + @Override + public void close() { + opened = false; + } + + @Override + public boolean ping() { + return false; + } + + @Override + public boolean ping(long sessionID) { + return false; + } + + public AuthResult authenticate(String user, String password) + throws AuthFailedException, IOErrorException, ClientServerIncompatibleException { + throw new UnsupportedOperationException( + "Direct connection authentication is not supported by the v5 driver; " + + "use NebulaPool.getSession(user, password, reconnect)."); + } +} diff --git a/client-v3compat/src/test/java/com/vesoft/nebula/driver/v3client/graph/V3IntegrationTest.java b/client-v3compat/src/test/java/com/vesoft/nebula/driver/v3client/graph/V3IntegrationTest.java new file mode 100644 index 000000000..bb264de72 --- /dev/null +++ b/client-v3compat/src/test/java/com/vesoft/nebula/driver/v3client/graph/V3IntegrationTest.java @@ -0,0 +1,199 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.Node; +import com.vesoft.nebula.driver.v3client.graph.data.PathWrapper; +import com.vesoft.nebula.driver.v3client.graph.data.Relationship; +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import com.vesoft.nebula.driver.v3client.graph.data.ValueWrapper; +import com.vesoft.nebula.driver.v3client.graph.net.NebulaPool; +import com.vesoft.nebula.driver.v3client.graph.net.Session; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * End-to-end integration test for the v3-compatible layer against a live NebulaGraph v5 cluster. + * + *

Disabled by default (so plain {@code mvn test} stays green without a cluster). Enable and + * point it at a cluster with: + * + *

+ *   mvn -pl client-v3compat test \
+ *       -Dnebula.it=true \
+ *       -Dnebula.host=127.0.0.1 -Dnebula.port=9669 \
+ *       -Dnebula.user=root -Dnebula.password=nebula \
+ *       -Dtest=V3IntegrationTest
+ * 
+ */ +public class V3IntegrationTest { + + private static final String HOST = System.getProperty("nebula.host", "127.0.0.1"); + private static final int PORT = Integer.getInteger("nebula.port", 9669); + private static final String USER = System.getProperty("nebula.user", "root"); + private static final String PASSWORD = System.getProperty("nebula.password", "nebula"); + + private static final long SUFFIX = System.currentTimeMillis(); + private static final String GRAPH = "it_graph_" + SUFFIX; + private static final String GRAPH_TYPE = "it_graph_type_" + SUFFIX; + + private static NebulaPool pool; + private static Session session; + + @BeforeClass + public static void setUp() throws Exception { + Assume.assumeTrue("integration test disabled (run with -Dnebula.it=true)", + Boolean.getBoolean("nebula.it")); + + List addresses = Arrays.asList(new HostAddress(HOST, PORT)); + NebulaPoolConfig poolConfig = new NebulaPoolConfig().setMaxConnSize(10).setTimeout(5000); + pool = new NebulaPool(); + assertTrue("pool init failed", pool.init(addresses, poolConfig)); + session = pool.getSession(USER, PASSWORD, false); + + exec("CREATE GRAPH TYPE IF NOT EXISTS " + GRAPH_TYPE + " AS {" + + "NODE TYPE nt_player (LABEL player {id INT PRIMARY KEY, name STRING, age INT})," + + "EDGE TYPE et_follow(nt_player)-[LABEL follow {degree INT}]->(nt_player)}"); + Thread.sleep(3000); + exec("CREATE GRAPH IF NOT EXISTS " + GRAPH + " " + GRAPH_TYPE); + Thread.sleep(3000); + exec("TABLE t{id,name,age} = (1,\"Tim\",36),(2,\"Jerry\",24) " + + "USE " + GRAPH + " " + + "FOR r IN t INSERT OR IGNORE(@nt_player{id:r.id,name:r.name,age:r.age})"); + exec("TABLE t{id1,id2,degree} = (1,2,90) " + + "USE " + GRAPH + " " + + "FOR r IN t " + + "OPTIONAL MATCH(src_node) WHERE src_node.id=r.id1 " + + "OPTIONAL MATCH(dst_node) WHERE dst_node.id=r.id2 " + + "INSERT OR IGNORE (src_node)-[@et_follow{degree:r.degree}]->(dst_node)"); + } + + @AfterClass + public static void tearDown() { + if (session != null) { + try { + exec("DROP GRAPH IF EXISTS " + GRAPH); + exec("DROP GRAPH TYPE IF EXISTS " + GRAPH_TYPE); + } catch (Exception ignored) { + // ignore cleanup failures + } + } + if (session != null) { + session.release(); + } + if (pool != null) { + pool.close(); + } + } + + @Test + public void testQueryNode() throws Exception { + ResultSet rs = exec("USE " + GRAPH + " MATCH (v:player) RETURN v ORDER BY v.id LIMIT 1"); + assertTrue(rs.isSucceeded()); + assertEquals(1, rs.rowsSize()); + assertTrue(rs.getColumnNames().contains("v")); + + ValueWrapper value = rs.rowValues(0).get("v"); + assertTrue(value.isVertex()); + assertFalse(value.isEdge()); + + Node node = value.asNode(); + assertTrue(node.getId().isLong()); + assertTrue(node.getId().asLong() != 0); + assertEquals(Arrays.asList("player"), node.tagNames()); + assertTrue(node.hasTagName("player")); + + Map props = node.properties("player"); + assertEquals("Tim", props.get("name").asString()); + assertEquals(36L, props.get("age").asLong()); + } + + @Test + public void testQueryEdge() throws Exception { + ResultSet rs = exec("USE " + GRAPH + " MATCH ()-[e:follow]->() RETURN e LIMIT 1"); + assertTrue(rs.isSucceeded()); + assertEquals(1, rs.rowsSize()); + + ValueWrapper value = rs.rowValues(0).get("e"); + assertTrue(value.isEdge()); + Relationship rel = value.asRelationship(); + assertEquals("follow", rel.edgeName()); + assertTrue(rel.srcId().asLong() != 0); + assertTrue(rel.dstId().asLong() != 0); + assertNotNull(rel.properties().get("degree")); + assertEquals(90L, rel.properties().get("degree").asLong()); + } + + @Test + public void testQueryPath() throws Exception { + ResultSet rs = exec( + "USE " + GRAPH + " MATCH p=(a:player)-[e:follow]->(b:player) RETURN p LIMIT 1"); + assertTrue(rs.isSucceeded()); + + ValueWrapper value = rs.rowValues(0).get("p"); + assertTrue(value.isPath()); + PathWrapper path = value.asPath(); + assertEquals(1, path.length()); + assertEquals(2, path.getNodes().size()); + assertEquals(1, path.getRelationships().size()); + assertEquals("follow", path.getRelationships().get(0).edgeName()); + assertNotNull(path.getStartNode()); + assertNotNull(path.getEndNode()); + } + + @Test + public void testScalarValueTypes() throws Exception { + ResultSet rs = exec( + "USE " + GRAPH + " RETURN 1 AS i, \"hello\" AS s, 3.5 AS d, true AS b LIMIT 1"); + assertTrue(rs.isSucceeded()); + ValueWrapper i = rs.rowValues(0).get("i"); + ValueWrapper s = rs.rowValues(0).get("s"); + ValueWrapper d = rs.rowValues(0).get("d"); + ValueWrapper b = rs.rowValues(0).get("b"); + + assertTrue(i.isLong()); + assertEquals(1L, i.asLong()); + assertTrue(s.isString()); + assertEquals("hello", s.asString()); + assertTrue(d.isDouble()); + assertEquals(3.5d, d.asDouble(), 0.0); + assertTrue(b.isBoolean()); + assertTrue(b.asBoolean()); + } + + @Test + public void testSessionPool() throws Exception { + SessionPoolConfig config = new SessionPoolConfig( + Arrays.asList(new HostAddress(HOST, PORT)), GRAPH, USER, PASSWORD) + .setMaxSessionSize(5).setMinSessionSize(1).setRetryConnectTimes(2).setWaitTime(100); + SessionPool sessionPool = new SessionPool(config); + try { + assertTrue(sessionPool.isActive()); + ResultSet rs = sessionPool.execute("MATCH (v:player) RETURN v LIMIT 1"); + assertTrue(rs.isSucceeded()); + assertTrue(rs.rowValues(0).get("v").isVertex()); + } finally { + sessionPool.close(); + } + } + + private static ResultSet exec(String gql) throws Exception { + ResultSet rs = session.execute(gql); + assertTrue("query failed: " + gql + " -> " + rs.getErrorMessage(), rs.isSucceeded()); + return rs; + } +} diff --git a/client-v3compat/src/test/java/com/vesoft/nebula/driver/v3client/graph/data/ValueWrapperTest.java b/client-v3compat/src/test/java/com/vesoft/nebula/driver/v3client/graph/data/ValueWrapperTest.java new file mode 100644 index 000000000..5f437c2d3 --- /dev/null +++ b/client-v3compat/src/test/java/com/vesoft/nebula/driver/v3client/graph/data/ValueWrapperTest.java @@ -0,0 +1,94 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.data; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.vesoft.nebula.driver.graph.decode.ColumnType; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.Test; + +public class ValueWrapperTest { + + private static com.vesoft.nebula.driver.graph.data.ValueWrapper v5(Object value, + ColumnType type) { + return new com.vesoft.nebula.driver.graph.data.ValueWrapper(value, type); + } + + @Test + public void testScalarTypes() throws Exception { + ValueWrapper longValue = new ValueWrapper(v5(42L, ColumnType.COLUMN_TYPE_INT64)); + assertTrue(longValue.isLong()); + assertFalse(longValue.isDouble()); + assertEquals(42L, longValue.asLong()); + + ValueWrapper intValue = new ValueWrapper(v5(7, ColumnType.COLUMN_TYPE_INT32)); + assertTrue(intValue.isLong()); + assertEquals(7L, intValue.asLong()); + + ValueWrapper boolValue = new ValueWrapper(v5(true, ColumnType.COLUMN_TYPE_BOOL)); + assertTrue(boolValue.isBoolean()); + assertEquals(true, boolValue.asBoolean()); + + ValueWrapper doubleValue = new ValueWrapper(v5(3.5d, ColumnType.COLUMN_TYPE_FLOAT64)); + assertTrue(doubleValue.isDouble()); + assertEquals(3.5d, doubleValue.asDouble(), 0.0); + + ValueWrapper stringValue = new ValueWrapper(v5("hello", ColumnType.COLUMN_TYPE_STRING)); + assertTrue(stringValue.isString()); + assertEquals("hello", stringValue.asString()); + + ValueWrapper nullValue = new ValueWrapper(v5(null, ColumnType.COLUMN_TYPE_ANY)); + assertTrue(nullValue.isNull()); + assertEquals(ValueWrapper.NullType.__NULL__, nullValue.asNull().getNullType()); + } + + @Test + public void testOfLong() throws Exception { + ValueWrapper id = ValueWrapper.ofLong(123L); + assertTrue(id.isLong()); + assertEquals(123L, id.asLong()); + } + + @Test + public void testList() throws Exception { + List list = new ArrayList<>(); + list.add(v5(1L, ColumnType.COLUMN_TYPE_INT64)); + list.add(v5("a", ColumnType.COLUMN_TYPE_STRING)); + ValueWrapper listValue = new ValueWrapper(v5(list, ColumnType.COLUMN_TYPE_LIST)); + assertTrue(listValue.isList()); + assertEquals(2, listValue.asList().size()); + assertEquals(1L, listValue.asList().get(0).asLong()); + assertEquals("a", listValue.asList().get(1).asString()); + } + + @Test + public void testSet() throws Exception { + Set set = new HashSet<>(); + set.add(v5(1L, ColumnType.COLUMN_TYPE_INT64)); + set.add(v5(2L, ColumnType.COLUMN_TYPE_INT64)); + ValueWrapper setValue = new ValueWrapper(v5(set, ColumnType.COLUMN_TYPE_SET)); + assertTrue(setValue.isSet()); + assertEquals(2, setValue.asSet().size()); + } + + @Test + public void testMap() throws Exception { + Map map = new HashMap<>(); + map.put(v5("k", ColumnType.COLUMN_TYPE_STRING), v5(1L, ColumnType.COLUMN_TYPE_INT64)); + ValueWrapper mapValue = new ValueWrapper(v5(map, ColumnType.COLUMN_TYPE_MAP)); + assertTrue(mapValue.isMap()); + assertEquals(1L, mapValue.asMap().get("k").asLong()); + } +} diff --git a/client-v3compat/src/test/java/com/vesoft/nebula/driver/v3client/graph/net/SessionParameterTest.java b/client-v3compat/src/test/java/com/vesoft/nebula/driver/v3client/graph/net/SessionParameterTest.java new file mode 100644 index 000000000..f0cf1522a --- /dev/null +++ b/client-v3compat/src/test/java/com/vesoft/nebula/driver/v3client/graph/net/SessionParameterTest.java @@ -0,0 +1,62 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula.driver.v3client.graph.net; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class SessionParameterTest { + + @Test + public void testScalarLiterals() { + assertEquals("NULL", Session.value2GqlLiteral(null)); + assertEquals("true", Session.value2GqlLiteral(true)); + assertEquals("3", Session.value2GqlLiteral(3)); + assertEquals("3.3", Session.value2GqlLiteral(3.3d)); + assertEquals("\"hello\"", Session.value2GqlLiteral("hello")); + assertEquals("\"a\\\"b\"", Session.value2GqlLiteral("a\"b")); + } + + @Test + public void testListLiteral() { + List list = new ArrayList<>(); + list.add(1); + list.add(true); + assertEquals("[1, true]", Session.value2GqlLiteral(list)); + } + + @Test + public void testMapLiteral() { + Map map = new HashMap<>(); + map.put("a", 1); + map.put("b", true); + assertEquals("{a: 1, b: true}", Session.value2GqlLiteral(map)); + } + + @Test + public void testInlineParameters() { + Map params = new HashMap<>(); + params.put("p1", 3); + params.put("p2", true); + params.put("name", "Tom"); + String stmt = "RETURN $p1 + 1, $p2, $name"; + assertEquals("RETURN 3 + 1, true, \"Tom\"", Session.inlineParameters(stmt, params)); + } + + @Test + public void testInlinePrefixCollision() { + Map params = new HashMap<>(); + params.put("p1", 1); + params.put("p10", 10); + // longest key replaced first to avoid $p1 clobbering $p10 + assertEquals("RETURN 10, 1", Session.inlineParameters("RETURN $p10, $p1", params)); + } +} diff --git a/examples/pom.xml b/examples/pom.xml index 4e88a25ab..3206002d4 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -25,6 +25,11 @@ driver 5.3-SNAPSHOT + + com.vesoft + client-v3compat + 5.3-SNAPSHOT + org.slf4j slf4j-log4j12 diff --git a/examples/src/main/java/com/vesoft/nebula/V3CompatExample.java b/examples/src/main/java/com/vesoft/nebula/V3CompatExample.java new file mode 100644 index 000000000..a7daa7cc0 --- /dev/null +++ b/examples/src/main/java/com/vesoft/nebula/V3CompatExample.java @@ -0,0 +1,65 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula; + +import com.vesoft.nebula.driver.v3client.graph.NebulaPoolConfig; +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import com.vesoft.nebula.driver.v3client.graph.net.NebulaPool; +import com.vesoft.nebula.driver.v3client.graph.net.Session; +import java.util.Arrays; +import java.util.List; + +/** + * Demonstrates the v3-compatible client surface running against NebulaGraph v5. + * + *

This is a migration example: it uses the v3 client API shape (NebulaPool + Session + + * ResultSet) from the {@code com.vesoft.nebula.driver.v3client} namespace, and only the GQL + * statements follow the v5 ISO-GQL dialect. + */ +public class V3CompatExample { + + public static void main(String[] args) { + if (args.length != 3) { + System.out.println("Usage: V3CompatExample

"); + System.exit(1); + } + String address = args[0]; + String user = args[1]; + String password = args[2]; + + List addresses = + Arrays.asList(new HostAddress(address.split(":")[0], + Integer.parseInt(address.split(":")[1]))); + + NebulaPoolConfig poolConfig = new NebulaPoolConfig(); + poolConfig.setMaxConnSize(10); + poolConfig.setMinConnSize(0); + poolConfig.setTimeout(1000); + + NebulaPool pool = new NebulaPool(); + try { + pool.init(addresses, poolConfig); + Session session = pool.getSession(user, password, false); + ResultSet resultSet = session.execute("RETURN 1+1 AS result"); + if (!resultSet.isSucceeded()) { + System.out.println("Query failed: " + resultSet.getErrorMessage()); + session.release(); + pool.close(); + System.exit(1); + } + if (resultSet.rowsSize() > 0) { + System.out.println("result = " + + resultSet.rowValues(0).get("result").asLong()); + } + session.release(); + pool.close(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/examples/src/main/java/com/vesoft/nebula/V3SessionPoolExample.java b/examples/src/main/java/com/vesoft/nebula/V3SessionPoolExample.java new file mode 100644 index 000000000..ca9467751 --- /dev/null +++ b/examples/src/main/java/com/vesoft/nebula/V3SessionPoolExample.java @@ -0,0 +1,153 @@ +/* Copyright (c) 2025 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +package com.vesoft.nebula; + +import com.vesoft.nebula.driver.v3client.graph.NebulaPoolConfig; +import com.vesoft.nebula.driver.v3client.graph.SessionPool; +import com.vesoft.nebula.driver.v3client.graph.SessionPoolConfig; +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import com.vesoft.nebula.driver.v3client.graph.exception.AuthFailedException; +import com.vesoft.nebula.driver.v3client.graph.exception.BindSpaceFailedException; +import com.vesoft.nebula.driver.v3client.graph.exception.ClientServerIncompatibleException; +import com.vesoft.nebula.driver.v3client.graph.exception.IOErrorException; +import com.vesoft.nebula.driver.v3client.graph.net.NebulaPool; +import com.vesoft.nebula.driver.v3client.graph.net.Session; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Mirrors the v3 client's {@code GraphSessionPoolExample}, but runs against NebulaGraph v5 through + * the v3-compatible namespace ({@code com.vesoft.nebula.driver.v3client}). + * + *

Usage: {@code V3SessionPoolExample [address] [user] [password]}. Defaults to + * {@code 127.0.0.1:9669 root nebula}. + */ +public class V3SessionPoolExample { + private static final Logger log = LoggerFactory.getLogger(V3SessionPoolExample.class); + + private static final String GRAPH_NAME = "test"; + + public static void main(String[] args) { + String address = args.length > 0 ? args[0] : "127.0.0.1:9669"; + String user = args.length > 1 ? args[1] : "root"; + String password = args.length > 2 ? args[2] : "nebula"; + + prepare(address, user, password); + + List addresses = toAddresses(address); + SessionPoolConfig sessionPoolConfig = + new SessionPoolConfig(addresses, GRAPH_NAME, user, password) + .setMaxSessionSize(10) + .setMinSessionSize(10) + .setRetryConnectTimes(3) + .setWaitTime(100) + .setRetryTimes(3) + .setIntervalTime(100); + SessionPool sessionPool = new SessionPool(sessionPoolConfig); + if (!sessionPool.init()) { + log.error("session pool init failed."); + return; + } + + ResultSet resultSet; + try { + resultSet = sessionPool.execute("MATCH (v:player) RETURN v LIMIT 1"); + System.out.println(resultSet.toString()); + } catch (IOErrorException | ClientServerIncompatibleException | AuthFailedException + | BindSpaceFailedException e) { + e.printStackTrace(); + sessionPool.close(); + System.exit(1); + } + + // execute in multiple threads + ExecutorService executorService = Executors.newFixedThreadPool(5); + for (int i = 0; i < 5; i++) { + executorService.submit(() -> { + try { + ResultSet result = sessionPool.execute("MATCH (v:player) RETURN v LIMIT 1"); + System.out.println(result.toString()); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + executorService.shutdown(); + try { + executorService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + sessionPool.close(); + } + + /** + * Create the graph type, graph and sample data using the connection-level API + * ({@code NebulaPool + Session}), mirroring the v3 example's prepare step. + */ + private static void prepare(String address, String user, String password) { + NebulaPool pool = new NebulaPool(); + NebulaPoolConfig nebulaPoolConfig = new NebulaPoolConfig(); + nebulaPoolConfig.setMaxConnSize(100); + List addresses = toAddresses(address); + try { + if (!pool.init(addresses, nebulaPoolConfig)) { + log.error("pool init failed."); + return; + } + Session session = pool.getSession(user, password, false); + + String createGraphType = "CREATE GRAPH TYPE IF NOT EXISTS graph_type_test AS {" + + "NODE TYPE node_type_player (LABEL player {id INT PRIMARY KEY, " + + "name STRING, age INT})}"; + ResultSet resp = session.execute(createGraphType); + check(resp, createGraphType); + + String createGraph = "CREATE GRAPH IF NOT EXISTS " + GRAPH_NAME + " graph_type_test"; + resp = session.execute(createGraph); + check(resp, createGraph); + + String insertNodes = "TABLE t{id,name,age} = " + + "(1,\"Tim\",36),(2,\"Jerry\",24),(3,\"Kyle\",30) " + + "USE " + GRAPH_NAME + " " + + "FOR r IN t " + + "INSERT OR IGNORE(@node_type_player{id:r.id,name:r.name,age:r.age})"; + resp = session.execute(insertNodes); + check(resp, insertNodes); + + session.release(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } finally { + pool.close(); + } + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private static void check(ResultSet resp, String stmt) { + if (!resp.isSucceeded()) { + log.error(String.format("Execute: `%s', failed: %s", stmt, resp.getErrorMessage())); + System.exit(1); + } + } + + private static List toAddresses(String address) { + String[] parts = address.split(":"); + return Arrays.asList(new HostAddress(parts[0], Integer.parseInt(parts[1]))); + } +} diff --git a/migration_guide_v3.md b/migration_guide_v3.md new file mode 100644 index 000000000..69af466dc --- /dev/null +++ b/migration_guide_v3.md @@ -0,0 +1,145 @@ +# Migrating from the NebulaGraph v3 Java client + +`client-v3compat` is a source-compatible re-implementation of the NebulaGraph v3 Java client +(`com.vesoft.nebula.client.graph.*`) under the `com.vesoft.nebula.driver.v3client` namespace. It +keeps the v3 API surface (class names and method signatures) and delegates internally to the v5 +`driver`, so existing v3 applications can migrate to NebulaGraph v5 with minimal changes. + +## Migration steps + +### 1. Replace the Maven dependency + +```xml + + + com.vesoft + client + 3.x.x + + + + + com.vesoft + client-v3compat + 5.3-SNAPSHOT + +``` + +### 2. Rewrite the imports + +Global replacement: + +- `com.vesoft.nebula.client.` → `com.vesoft.nebula.driver.v3client.` +- `com.vesoft.nebula.ErrorCode` → `com.vesoft.nebula.driver.v3client.graph.ErrorCode` + +### 3. Migrate the GQL from nGQL to ISO-GQL + +The compatibility layer adapts the Java API and data model only — it does **not** rewrite query +text. Migrate your statements yourself. Typical rewrites: + +| v3 nGQL | v5 ISO-GQL | +|---|---| +| `CREATE SPACE ... (vid_type=...)` | `CREATE GRAPH TYPE ... AS {...}` + `CREATE GRAPH ... ` | +| `CREATE TAG ...` / `CREATE EDGE ...` | `NODE TYPE` / `EDGE TYPE` inside `CREATE GRAPH TYPE` | +| `INSERT VERTEX ... VALUES ...` | `INSERT OR IGNORE(@node_type{...})` | +| `INSERT EDGE ... VALUES ...` | `INSERT OR IGNORE (src)-[@edge_type{...}]->(dst)` | +| `USE space;` | `USE graph` or `SESSION SET GRAPH "graph"` | +| `GO ...` / `FETCH ...` / `LOOKUP ...` | `MATCH ...` | +| `YIELD` | `RETURN` | + +## Code examples + +### NebulaPool + Session + +```java +import com.vesoft.nebula.driver.v3client.graph.NebulaPoolConfig; +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import com.vesoft.nebula.driver.v3client.graph.net.NebulaPool; +import com.vesoft.nebula.driver.v3client.graph.net.Session; +import java.util.Arrays; + +NebulaPool pool = new NebulaPool(); +NebulaPoolConfig config = new NebulaPoolConfig(); +config.setMaxConnSize(10); +pool.init(Arrays.asList(new HostAddress("127.0.0.1", 9669)), config); + +Session session = pool.getSession("root", "nebula", false); +ResultSet rs = session.execute("MATCH (v:player) RETURN v LIMIT 1"); // ISO-GQL +if (rs.isSucceeded()) { + ResultSet.Record rec = rs.rowValues(0); + System.out.println(rec.get("v")); +} +session.release(); +pool.close(); +``` + +### SessionPool + +```java +import com.vesoft.nebula.driver.v3client.graph.SessionPool; +import com.vesoft.nebula.driver.v3client.graph.SessionPoolConfig; +import com.vesoft.nebula.driver.v3client.graph.data.HostAddress; +import com.vesoft.nebula.driver.v3client.graph.data.ResultSet; +import java.util.Arrays; + +SessionPoolConfig config = new SessionPoolConfig( + Arrays.asList(new HostAddress("127.0.0.1", 9669)), "my_graph", "root", "nebula") + .setMaxSessionSize(10) + .setMinSessionSize(1); +SessionPool pool = new SessionPool(config); +ResultSet rs = pool.execute("MATCH (v:player) RETURN v LIMIT 1"); // bound to my_graph +pool.close(); +``` + +Full runnable examples: `examples/src/main/java/com/vesoft/nebula/V3CompatExample.java` and +`examples/src/main/java/com/vesoft/nebula/V3SessionPoolExample.java`. + +## Coverage + +Only the v3 `graph` package is covered: + +- `graph`: `NebulaPoolConfig`, `NebulaSession`, `SessionPool`, `SessionPoolConfig`, + `SessionsManagerConfig` +- `graph.data`: `ResultSet`(+`Record`), `ValueWrapper`(+`NullType`), `Node`, `Relationship`, + `PathWrapper`(+`Segment`), `DateWrapper`/`TimeWrapper`/`DateTimeWrapper`/`DurationWrapper`/ + `GeographyWrapper` + geographic wrappers, `HostAddress`, `SSLParam` hierarchy, `TimeUtil` +- `graph.exception`: `AuthFailedException`, `BindSpaceFailedException`, + `ClientServerIncompatibleException`, `IOErrorException`, `InvalidConfigException`, + `InvalidSessionException`, `InvalidValueException`, `NotValidConnectionException` +- `graph.net`: `NebulaPool`, `Session`, `SessionState`, `SessionsManager`, `SessionWrapper`, + `AuthResult` and connection-level shims (`Connection`, `SyncConnection`, `LoadBalancer`, + `RoundRobinLoadBalancer`, `ConnObjectPool`) + +The v3 `meta`, `storage`, and `encoder` packages have no v5 equivalents and are **not** provided. + +## Known differences + +The following v3 API details cannot be reproduced verbatim in v5: + +| v3 API | Behavior in the compat layer | Reason | +|---|---|---| +| `ValueWrapper.getValue()` returns `Value` | returns the underlying v5 `ValueWrapper` (as `Object`) | v3 Thrift `Value` no longer exists | +| `ResultSet.getRows()` returns `List` | removed | v3 Thrift `Row` no longer exists | +| `ResultSet.getPlanDesc()` returns `PlanDescription` | returns the v5 `PlanInfoNode` | plan-tree type changed | +| `ResultSet.getSpaceName()` / `getComment()` | returns `""` | no equivalent field in the v5 response | +| `Node.getId()` / `Relationship.srcId()/dstId()` string vid | returns the v5 numeric id as a `ValueWrapper` (`asLong()`) | v5 ids are `long`; string ids are not recoverable | +| v3 multi-tag vertex `values(tagName)` | returns the flat property map; `tagName` is only validated | v5 is a single node type + labels with flat properties | +| `executeJson` JSON structure | generated via fastjson (approximate v3 shape) | v5 has no JSON channel | +| v5 `DECIMAL` values | surfaced via `ValueWrapper.isDouble()` / `asDouble()` | v3 had no decimal type; converted to double | + +## Build & test + +```bash +# unit tests +mvn -pl client-v3compat test + +# integration test against a live v5 cluster +mvn -pl client-v3compat test \ + -Dnebula.it=true -Dnebula.host= -Dnebula.port= \ + -Dnebula.user=root -Dnebula.password= \ + -Dtest=V3IntegrationTest +``` + +The integration test creates a graph type + graph, inserts nodes and edges, queries node/edge/path, +exercises `SessionPool`, and drops everything afterwards. diff --git a/pom.xml b/pom.xml index 47092bd73..5b85d0367 100644 --- a/pom.xml +++ b/pom.xml @@ -10,6 +10,7 @@ pom client + client-v3compat examples