From fc7d5857b4165c16bb6e07ae1fddd0ff7486d999 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Wed, 11 Mar 2026 22:11:08 +0800 Subject: [PATCH 01/60] add java sdk --- proto/response.proto | 2 + tools/java/USAGE.md | 186 +++++++ tools/java/pom.xml | 180 +++++++ .../java/org/alibaba/neug/driver/Driver.java | 70 +++ .../alibaba/neug/driver/GraphDatabase.java | 67 +++ .../org/alibaba/neug/driver/ResultSet.java | 245 +++++++++ .../java/org/alibaba/neug/driver/Session.java | 90 ++++ .../neug/driver/internal/InternalDriver.java | 51 ++ .../driver/internal/InternalResultSet.java | 473 ++++++++++++++++++ .../neug/driver/internal/InternalSession.java | 83 +++ .../alibaba/neug/driver/utils/AccessMode.java | 21 + .../org/alibaba/neug/driver/utils/Client.java | 69 +++ .../org/alibaba/neug/driver/utils/Config.java | 118 +++++ .../alibaba/neug/driver/utils/JsonUtil.java | 34 ++ .../neug/driver/utils/QuerySerializer.java | 55 ++ .../neug/driver/utils/ResponseParser.java | 30 ++ 16 files changed, 1774 insertions(+) create mode 100644 tools/java/USAGE.md create mode 100644 tools/java/pom.xml create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/Driver.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/GraphDatabase.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/ResultSet.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/Session.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/utils/Client.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/utils/Config.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java create mode 100644 tools/java/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java diff --git a/proto/response.proto b/proto/response.proto index 5bd5551c..08e2d8ac 100644 --- a/proto/response.proto +++ b/proto/response.proto @@ -15,6 +15,8 @@ */ syntax="proto3"; package neug; +option java_package = "org.alibaba.neug.driver"; +option java_outer_classname = "Results"; option cc_generic_services = true; diff --git a/tools/java/USAGE.md b/tools/java/USAGE.md new file mode 100644 index 00000000..af054024 --- /dev/null +++ b/tools/java/USAGE.md @@ -0,0 +1,186 @@ +# NeuG Java Driver 使用指南 + +## 在其他项目中使用 + +### 方式一:本地 Maven 仓库(开发测试) + +1. 安装到本地 Maven 仓库: +```bash +cd /data/0319/neug2/tools/java +mvn clean install -DskipTests +``` + +2. 在其他项目的 `pom.xml` 中添加依赖: +```xml + + org.alibaba.neug + neug-java-driver + 1.0.0-SNAPSHOT + +``` + +### 方式二:使用本地 JAR 文件 + +如果不想使用 Maven 仓库,可以直接引用编译好的 JAR: + +```xml + + org.alibaba.neug + neug-java-driver + 1.0.0-SNAPSHOT + system + /data/0319/neug2/tools/java/target/neug-java-driver-1.0.0-SNAPSHOT.jar + +``` + +### 方式三:发布到私有 Maven 仓库(生产环境推荐) + +1. 在 `pom.xml` 中添加 distributionManagement 配置: +```xml + + + your-releases + Your Release Repository + http://your-nexus-server/repository/maven-releases/ + + + your-snapshots + Your Snapshot Repository + http://your-nexus-server/repository/maven-snapshots/ + + +``` + +2. 在 `~/.m2/settings.xml` 中配置仓库认证信息: +```xml + + + your-releases + your-username + your-password + + + your-snapshots + your-username + your-password + + +``` + +3. 发布到仓库: +```bash +mvn clean deploy +``` + +## 使用示例 + +### 基本连接 + +```java +import org.alibaba.neug.driver.*; + +public class Example { + public static void main(String[] args) { + // 创建驱动 + Driver driver = GraphDatabase.driver("http://localhost:8000"); + + try { + // 验证连接 + driver.verifyConnectivity(); + + // 创建会话 + try (Session session = driver.session()) { + // 执行查询 + try (ResultSet rs = session.run("MATCH (n) RETURN n LIMIT 10")) { + while (rs.next()) { + System.out.println(rs.getObject("n")); + } + } + } + } finally { + driver.close(); + } + } +} +``` + +### 带配置的连接 + +```java +import org.alibaba.neug.driver.*; +import java.util.concurrent.TimeUnit; + +public class ConfigExample { + public static void main(String[] args) { + Config config = Config.builder() + .withConnectionTimeout(30, TimeUnit.SECONDS) + .withMaxRetries(3) + .build(); + + Driver driver = GraphDatabase.driver("http://localhost:8000", config); + + try (Session session = driver.session(AccessMode.READ)) { + // 只读查询 + try (ResultSet rs = session.run("MATCH (n:Person) RETURN n.name, n.age")) { + while (rs.next()) { + String name = rs.getString("n.name"); + int age = rs.getInt("n.age"); + System.out.println(name + ", " + age); + } + } + } finally { + driver.close(); + } + } +} +``` + +### 带参数的查询 + +```java +import java.util.HashMap; +import java.util.Map; + +Map parameters = new HashMap<>(); +parameters.put("name", "Alice"); +parameters.put("age", 30); + +try (Session session = driver.session()) { + String query = "CREATE (p:Person {name: $name, age: $age}) RETURN p"; + try (ResultSet rs = session.run(query, parameters)) { + if (rs.next()) { + System.out.println("Created: " + rs.getObject("p")); + } + } +} +``` + +## Gradle 项目使用 + +如果使用 Gradle,在 `build.gradle` 中添加: + +```groovy +dependencies { + implementation 'org.alibaba.neug:neug-java-driver:1.0.0-SNAPSHOT' +} +``` + +对于本地 JAR: +```groovy +dependencies { + implementation files('/data/0319/neug2/tools/java/target/neug-java-driver-1.0.0-SNAPSHOT.jar') + implementation 'com.squareup.okhttp3:okhttp:4.11.0' + implementation 'com.google.protobuf:protobuf-java:4.29.6' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' +} +``` + +## 依赖说明 + +该 driver 依赖以下库: +- OkHttp 4.11.0 - HTTP 客户端 +- Protocol Buffers 4.29.6 - 序列化 +- Jackson 2.15.2 - JSON 处理 +- SLF4J 2.0.7 - 日志接口 + +这些依赖会被 Maven 自动管理。 diff --git a/tools/java/pom.xml b/tools/java/pom.xml new file mode 100644 index 00000000..8edeaba5 --- /dev/null +++ b/tools/java/pom.xml @@ -0,0 +1,180 @@ + + + 4.0.0 + + org.alibaba.neug + neug-java-driver + 1.0.0-SNAPSHOT + jar + + NeuG Java Driver + Java driver for NeuG graph database + + + UTF-8 + 8 + 8 + 5.9.3 + 2.0.7 + 4.29.6 + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + + com.squareup.okhttp3 + okhttp + 4.9.0 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.mockito + mockito-core + 5.4.0 + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 8 + 8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.5.0 + + + attach-javadocs + + jar + + + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + ${project.basedir}/../../proto + + response.proto + + ${project.build.directory}/generated-sources/protobuf/java + false + + + + + compile + + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.1 + true + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.40.0 + + + + src/main/java/**/*.java + src/test/java/**/*.java + + + 1.17.0 + + + + + + + + + + + diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/Driver.java b/tools/java/src/main/java/org/alibaba/neug/driver/Driver.java new file mode 100644 index 00000000..0cc6019e --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/Driver.java @@ -0,0 +1,70 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import java.io.Closeable; + +/** + * The main driver interface for NeuG database connections. + * + *

A driver manages database connections and provides sessions for executing queries. + * It is responsible for connection pooling, resource management, and cleanup. + * + *

Example usage: + * + *

{@code
+ * Driver driver = GraphDatabase.driver("http://localhost:8080");
+ * try (Session session = driver.session()) {
+ *     ResultSet results = session.run("MATCH (n) RETURN n");
+ *     // Process results
+ * } finally {
+ *     driver.close();
+ * }
+ * }
+ */ +public interface Driver extends Closeable { + + /** + * Creates a new session for executing queries. + * + *

Sessions should be closed after use to release resources. + * + * @return a new {@link Session} instance + */ + Session session(); + + /** + * Verifies that the driver can connect to the database server. + * + *

This method attempts to establish a connection and verify basic connectivity. + * + * @throws RuntimeException if the connection cannot be established + */ + void verifyConnectivity(); + + /** + * Closes the driver and releases all associated resources. + * + *

After calling this method, the driver should not be used anymore. + */ + @Override + void close(); + + /** + * Checks whether the driver has been closed. + * + * @return {@code true} if the driver is closed, {@code false} otherwise + */ + boolean isClosed(); +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/GraphDatabase.java b/tools/java/src/main/java/org/alibaba/neug/driver/GraphDatabase.java new file mode 100644 index 00000000..63c4dcfd --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/GraphDatabase.java @@ -0,0 +1,67 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import org.alibaba.neug.driver.internal.InternalDriver; +import org.alibaba.neug.driver.utils.Config; + +/** + * Main entry point for creating NeuG database driver connections. + * + *

This class provides factory methods to create {@link Driver} instances that can be used to + * interact with a NeuG graph database. + * + *

Example usage: + * + *

{@code
+ * // Create a driver with default configuration
+ * Driver driver = GraphDatabase.driver("http://localhost:8080");
+ *
+ * // Create a driver with custom configuration
+ * Config config = Config.builder()
+ *     .withMaxConnectionPoolSize(10)
+ *     .build();
+ * Driver driver = GraphDatabase.driver("http://localhost:8080", config);
+ * }
+ */ +public final class GraphDatabase { + + private GraphDatabase() { + // Prevent instantiation + } + + /** + * Creates a new driver instance with default configuration. + * + * @param uri the URI of the NeuG database server (e.g., "http://localhost:8080") + * @return a new {@link Driver} instance + * @throws IllegalArgumentException if the URI is null or invalid + */ + public static Driver driver(String uri) { + return new InternalDriver(uri, Config.builder().build()); + } + + /** + * Creates a new driver instance with custom configuration. + * + * @param uri the URI of the NeuG database server (e.g., "http://localhost:8080") + * @param config the configuration settings for the driver + * @return a new {@link Driver} instance + * @throws IllegalArgumentException if the URI or config is null or invalid + */ + public static Driver driver(String uri, Config config) { + return new InternalDriver(uri, config); + } +} +; diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/ResultSet.java b/tools/java/src/main/java/org/alibaba/neug/driver/ResultSet.java new file mode 100644 index 00000000..74c655d3 --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/ResultSet.java @@ -0,0 +1,245 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import java.sql.Date; +import java.sql.Timestamp; +import java.util.List; + +/** + * A cursor over the results of a database query. + * + *

A ResultSet maintains a cursor pointing to its current row of data. Initially the cursor is + * positioned before the first row. The {@link #next()} method moves the cursor to the next row. + * + *

Example usage: + * + *

{@code
+ * ResultSet results = session.run("MATCH (n:Person) RETURN n.name, n.age");
+ * while (results.next()) {
+ *     String name = results.getString("n.name");
+ *     int age = results.getInt("n.age");
+ *     System.out.println(name + " is " + age + " years old");
+ * }
+ * results.close();
+ * }
+ */ +public interface ResultSet extends AutoCloseable { + /** + * Moves the cursor forward one row from its current position. + * + * @return {@code true} if the new current row is valid; {@code false} if there are no more rows + */ + boolean next(); + + /** + * Moves the cursor backward one row from its current position. + * + * @return {@code true} if the new current row is valid; {@code false} if the cursor is before + * the first row + */ + boolean previous(); + + /** + * Moves the cursor to the given row number in this ResultSet. + * + * @param row the row number to move to (0-based) + * @return {@code true} if the cursor is moved to a valid row; {@code false} otherwise + */ + boolean absolute(int row); + + /** + * Moves the cursor a relative number of rows, either positive or negative. + * + * @param rows the number of rows to move (positive for forward, negative for backward) + * @return {@code true} if the cursor is moved to a valid row; {@code false} otherwise + */ + boolean relative(int rows); + + /** + * Retrieves the current row number. + * + * @return the current row number (0-based), or -1 if there is no current row + */ + int getRow(); + + /** + * Retrieves the value of the designated column as an Object. + * + * @param columnName the name of the column + * @return the column value + */ + Object getObject(String columnName); + + /** + * Retrieves the value of the designated column as an Object. + * + * @param columnIndex the column index (0-based) + * @return the column value + */ + Object getObject(int columnIndex); + + /** + * Retrieves the value of the designated column as an int. + * + * @param columnName the name of the column + * @return the column value; 0 if the value is SQL NULL + */ + int getInt(String columnName); + + /** + * Retrieves the value of the designated column as an int. + * + * @param columnIndex the column index (0-based) + * @return the column value; 0 if the value is SQL NULL + */ + int getInt(int columnIndex); + + /** + * Retrieves the value of the designated column as a long. + * + * @param columnName the name of the column + * @return the column value; 0 if the value is SQL NULL + */ + long getLong(String columnName); + + /** + * Retrieves the value of the designated column as a long. + * + * @param columnIndex the column index (0-based) + * @return the column value; 0 if the value is SQL NULL + */ + long getLong(int columnIndex); + + /** + * Retrieves the value of the designated column as a String. + * + * @param columnName the name of the column + * @return the column value; {@code null} if the value is SQL NULL + */ + String getString(String columnName); + + /** + * Retrieves the value of the designated column as a String. + * + * @param columnIndex the column index (0-based) + * @return the column value; {@code null} if the value is SQL NULL + */ + String getString(int columnIndex); + + /** + * Retrieves the value of the designated column as a Date. + * + * @param columnName the name of the column + * @return the column value; {@code null} if the value is SQL NULL + */ + Date getDate(String columnName); + + /** + * Retrieves the value of the designated column as a Date. + * + * @param columnIndex the column index (0-based) + * @return the column value; {@code null} if the value is SQL NULL + */ + Date getDate(int columnIndex); + + /** + * Retrieves the value of the designated column as a Timestamp. + * + * @param columnName the name of the column + * @return the column value; {@code null} if the value is SQL NULL + */ + Timestamp getTimestamp(String columnName); + + /** + * Retrieves the value of the designated column as a Timestamp. + * + * @param columnIndex the column index (0-based) + * @return the column value; {@code null} if the value is SQL NULL + */ + Timestamp getTimestamp(int columnIndex); + + /** + * Retrieves the value of the designated column as a boolean. + * + * @param columnName the name of the column + * @return the column value; {@code false} if the value is SQL NULL + */ + boolean getBoolean(String columnName); + + /** + * Retrieves the value of the designated column as a boolean. + * + * @param columnIndex the column index (0-based) + * @return the column value; {@code false} if the value is SQL NULL + */ + boolean getBoolean(int columnIndex); + + /** + * Retrieves the value of the designated column as a double. + * + * @param columnName the name of the column + * @return the column value; 0 if the value is SQL NULL + */ + double getDouble(String columnName); + + /** + * Retrieves the value of the designated column as a double. + * + * @param columnIndex the column index (0-based) + * @return the column value; 0 if the value is SQL NULL + */ + double getDouble(int columnIndex); + + /** + * Retrieves the value of the designated column as a float. + * + * @param columnName the name of the column + * @return the column value; 0 if the value is SQL NULL + */ + float getFloat(String columnName); + + /** + * Retrieves the value of the designated column as a float. + * + * @param columnIndex the column index (0-based) + * @return the column value; 0 if the value is SQL NULL + */ + float getFloat(int columnIndex); + + /** + * Reports whether the last column read had a value of SQL NULL. + * + * @return {@code true} if the last column value read was SQL NULL; {@code false} otherwise + */ + boolean wasNull(); + + /** Closes this ResultSet and releases all associated resources. */ + @Override + void close(); + + /** + * Checks whether the ResultSet has been closed. + * + * @return {@code true} if the ResultSet is closed, {@code false} otherwise + */ + boolean isClosed(); + + /** + * Retrieves the names of all columns in this ResultSet. + * + * @return a list of column names + */ + List getColumnNames(); +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/Session.java b/tools/java/src/main/java/org/alibaba/neug/driver/Session.java new file mode 100644 index 00000000..b60f27f8 --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/Session.java @@ -0,0 +1,90 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import java.util.Map; +import org.alibaba.neug.driver.utils.AccessMode; + +/** + * A session for executing queries against a NeuG database. + * + *

Sessions are lightweight and should be closed after use. They provide methods for executing + * Cypher queries and retrieving results. + * + *

Example usage: + * + *

{@code
+ * try (Session session = driver.session()) {
+ *     ResultSet results = session.run("MATCH (n:Person) WHERE n.age > $age RETURN n",
+ *         Map.of("age", 30));
+ *     while (results.next()) {
+ *         System.out.println(results.getString("n"));
+ *     }
+ * }
+ * }
+ */ +public interface Session extends AutoCloseable { + /** + * Executes a Cypher statement and returns the results. + * + * @param statement the Cypher query to execute + * @return a {@link ResultSet} containing the query results + * @throws RuntimeException if the query fails + */ + ResultSet run(String statement); + + /** + * Executes a Cypher statement with configuration options. + * + * @param statement the Cypher query to execute + * @param config configuration options for query execution + * @return a {@link ResultSet} containing the query results + * @throws RuntimeException if the query fails + */ + ResultSet run(String statement, Map config); + + /** + * Executes a Cypher statement with a specific access mode. + * + * @param statement the Cypher query to execute + * @param mode the access mode (READ or WRITE) + * @return a {@link ResultSet} containing the query results + * @throws RuntimeException if the query fails + */ + ResultSet run(String statement, AccessMode mode); + + /** + * Executes a parameterized Cypher statement with a specific access mode. + * + * @param statement the Cypher query to execute + * @param parameters query parameters as key-value pairs + * @param mode the access mode (READ or WRITE) + * @return a {@link ResultSet} containing the query results + * @throws RuntimeException if the query fails + */ + ResultSet run(String statement, Map parameters, AccessMode mode); + + /** + * Closes the session and releases all associated resources. + */ + @Override + void close(); + + /** + * Checks whether the session has been closed. + * + * @return {@code true} if the session is closed, {@code false} otherwise + */ + boolean isClosed(); +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java b/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java new file mode 100644 index 00000000..d10d185c --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver.internal; + +import org.alibaba.neug.driver.Driver; +import org.alibaba.neug.driver.Session; +import org.alibaba.neug.driver.utils.AccessMode; +import org.alibaba.neug.driver.utils.Client; +import org.alibaba.neug.driver.utils.Config; + +public class InternalDriver implements Driver { + + private static Client client = null; + + public InternalDriver(String uri, Config config) { + client = new Client(uri, config); + } + + @Override + public Session session() { + return new InternalSession(client); + } + + @Override + public void verifyConnectivity() { + try (Session session = session()) { + session.run("RETURN 1", null, AccessMode.READ); + } + } + + @Override + public void close() { + client.close(); + } + + @Override + public boolean isClosed() { + return client.isClosed(); + } +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java new file mode 100644 index 00000000..a580ff75 --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java @@ -0,0 +1,473 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver.internal; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.protobuf.ByteString; +import java.sql.Date; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.alibaba.neug.driver.ResultSet; +import org.alibaba.neug.driver.Results; +import org.alibaba.neug.driver.utils.JsonUtil; + +public class InternalResultSet implements ResultSet { + public InternalResultSet(Results.QueryResponse response) { + this.response = response; + this.currentIndex = -1; + this.is_null = false; + this.closed = false; + } + + @Override + public boolean absolute(int row) { + if (row < 0 || row >= response.getRowCount()) { + return false; + } + currentIndex = row; + return true; + } + + @Override + public boolean relative(int rows) { + return absolute(currentIndex + rows); + } + + @Override + public boolean next() { + if (currentIndex + 1 < response.getRowCount()) { + currentIndex++; + return true; + } + return false; + } + + @Override + public boolean previous() { + if (currentIndex - 1 >= 0) { + currentIndex--; + return true; + } + return false; + } + + @Override + public int getRow() { + return currentIndex; + } + + @Override + public Object getObject(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getObject(columnIndex); + } + + @Override + public Object getObject(int columnIndex) { + // Return the appropriate type based on the array type + Results.Array array = response.getArrays(columnIndex); + try { + return getObject(array, currentIndex, false); + } catch (Exception e) { + throw new RuntimeException("Failed to get object from column " + columnIndex, e); + } + } + + private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted) + throws Exception { + switch (array.getTypedArrayCase()) { + case STRING_ARRAY: + { + if (!isNullSetted) { + ByteString nullBitmap = array.getStringArray().getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + return array.getStringArray().getValues(rowIndex); + } + case INT32_ARRAY: + { + if (!isNullSetted) { + ByteString nullBitmap = array.getInt32Array().getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + return array.getInt32Array().getValues(rowIndex); + } + case INT64_ARRAY: + { + if (!isNullSetted) { + ByteString nullBitmap = array.getInt64Array().getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + return array.getInt64Array().getValues(rowIndex); + } + case BOOL_ARRAY: + { + if (!isNullSetted) { + ByteString nullBitmap = array.getBoolArray().getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + return array.getBoolArray().getValues(rowIndex); + } + case DOUBLE_ARRAY: + { + if (!isNullSetted) { + ByteString nullBitmap = array.getDoubleArray().getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + return array.getDoubleArray().getValues(rowIndex); + } + case TIMESTAMP_ARRAY: + { + if (!isNullSetted) { + ByteString nullBitmap = array.getTimestampArray().getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + return array.getTimestampArray().getValues(rowIndex); + } + case DATE_ARRAY: + { + if (!isNullSetted) { + ByteString nullBitmap = array.getDateArray().getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + return array.getDateArray().getValues(rowIndex); + } + case LIST_ARRAY: + { + Results.ListArray listArray = array.getListArray(); + + if (!isNullSetted) { + ByteString nullBitmap = listArray.getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + + int start = listArray.getOffsets(rowIndex); + int end = listArray.getOffsets(rowIndex + 1); + List list = new ArrayList<>(end - start); + for (int i = start; i < end; i++) { + list.add(getObject(listArray.getElements(), i, true)); + } + return list; + } + case STRUCT_ARRAY: + { + Results.StructArray structArray = array.getStructArray(); + + if (!isNullSetted) { + ByteString nullBitmap = structArray.getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + List struct = new ArrayList<>(structArray.getFieldsCount()); + for (int i = 0; i < structArray.getFieldsCount(); i++) { + struct.add(getObject(structArray.getFields(i), rowIndex, true)); + } + return struct; + } + case VERTEX_ARRAY: + { + Results.VertexArray vertexArray = array.getVertexArray(); + ObjectMapper mapper = JsonUtil.getInstance(); + if (!isNullSetted) { + ByteString nullBitmap = vertexArray.getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + Map map = + mapper.readValue( + vertexArray.getValues(rowIndex), + new TypeReference>() {}); + return map; + } + case EDGE_ARRAY: + { + Results.EdgeArray edgeArray = array.getEdgeArray(); + ObjectMapper mapper = JsonUtil.getInstance(); + if (!isNullSetted) { + ByteString nullBitmap = edgeArray.getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + Map map = + mapper.readValue( + edgeArray.getValues(rowIndex), + new TypeReference>() {}); + return map; + } + case PATH_ARRAY: + { + Results.PathArray pathArray = array.getPathArray(); + ObjectMapper mapper = JsonUtil.getInstance(); + if (!isNullSetted) { + ByteString nullBitmap = pathArray.getValidity(); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + Map map = + mapper.readValue( + pathArray.getValues(rowIndex), + new TypeReference>() {}); + return map; + } + default: + throw new IllegalArgumentException( + "Unsupported array type: " + array.getTypedArrayCase()); + } + } + + @Override + public int getInt(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getInt(columnIndex); + } + + @Override + public int getInt(int columnIndex) { + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasInt32Array()) { + throw new IllegalArgumentException("Column " + columnIndex + " is not of type int32"); + } + Results.Int32Array array = arr.getInt32Array(); + ByteString nullBitmap = array.getValidity(); + int value = array.getValues(currentIndex); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return value; + } + + @Override + public long getLong(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getLong(columnIndex); + } + + @Override + public long getLong(int columnIndex) { + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasInt64Array()) { + throw new IllegalArgumentException("Column " + columnIndex + " is not of type int64"); + } + Results.Int64Array array = arr.getInt64Array(); + ByteString nullBitmap = array.getValidity(); + long value = array.getValues(currentIndex); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return value; + } + + @Override + public String getString(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getString(columnIndex); + } + + @Override + public String getString(int columnIndex) { + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasStringArray()) { + throw new IllegalArgumentException("Column " + columnIndex + " is not of type string"); + } + Results.StringArray array = arr.getStringArray(); + ByteString nullBitmap = array.getValidity(); + String value = array.getValues(currentIndex); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return value; + } + + @Override + public Date getDate(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getDate(columnIndex); + } + + @Override + public Date getDate(int columnIndex) { + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasDateArray()) { + throw new IllegalArgumentException("Column " + columnIndex + " is not of type date"); + } + Results.DateArray array = arr.getDateArray(); + ByteString nullBitmap = array.getValidity(); + long timestamp = array.getValues(currentIndex); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return new Date(timestamp); + } + + @Override + public Timestamp getTimestamp(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getTimestamp(columnIndex); + } + + @Override + public Timestamp getTimestamp(int columnIndex) { + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasTimestampArray()) { + throw new IllegalArgumentException( + "Column " + columnIndex + " is not of type timestamp"); + } + Results.TimestampArray array = arr.getTimestampArray(); + ByteString nullBitmap = array.getValidity(); + long timestamp = array.getValues(currentIndex); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return new Timestamp(timestamp); + } + + @Override + public boolean getBoolean(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getBoolean(columnIndex); + } + + @Override + public boolean getBoolean(int columnIndex) { + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasBoolArray()) { + throw new IllegalArgumentException("Column " + columnIndex + " is not of type boolean"); + } + Results.BoolArray array = arr.getBoolArray(); + ByteString nullBitmap = array.getValidity(); + boolean value = array.getValues(currentIndex); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return value; + } + + @Override + public double getDouble(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getDouble(columnIndex); + } + + @Override + public double getDouble(int columnIndex) { + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasDoubleArray()) { + throw new IllegalArgumentException("Column " + columnIndex + " is not of type double"); + } + Results.DoubleArray array = arr.getDoubleArray(); + ByteString nullBitmap = array.getValidity(); + double value = array.getValues(currentIndex); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return value; + } + + @Override + public float getFloat(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getFloat(columnIndex); + } + + @Override + public float getFloat(int columnIndex) { + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasFloatArray()) { + throw new IllegalArgumentException("Column " + columnIndex + " is not of type float"); + } + Results.FloatArray array = arr.getFloatArray(); + ByteString nullBitmap = array.getValidity(); + float value = array.getValues(currentIndex); + is_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return value; + } + + @Override + public boolean wasNull() { + return is_null; + } + + @Override + public List getColumnNames() { + Results.MetaDatas metaDatas = response.getSchema(); + List columnNames = new ArrayList<>(); + for (int i = 0; i < metaDatas.getNameCount(); i++) { + columnNames.add(metaDatas.getName(i)); + } + return columnNames; + } + + @Override + public void close() { + closed = true; + } + + @Override + public boolean isClosed() { + return closed; + } + + private int getColumnIndex(String columnName) { + Results.MetaDatas colum_name = response.getSchema(); + int columnCount = colum_name.getNameCount(); + for (int i = 0; i < columnCount; i++) { + if (colum_name.getName(i).equals(columnName)) { + return i; + } + } + throw new IllegalArgumentException("Column not found: " + columnName); + } + + private Results.QueryResponse response; + private int currentIndex; + private boolean is_null; + private boolean closed; +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java b/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java new file mode 100644 index 00000000..04b70ead --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java @@ -0,0 +1,83 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver.internal; + +import java.util.Map; +import org.alibaba.neug.driver.ResultSet; +import org.alibaba.neug.driver.Session; +import org.alibaba.neug.driver.utils.AccessMode; +import org.alibaba.neug.driver.utils.Client; +import org.alibaba.neug.driver.utils.QuerySerializer; +import org.alibaba.neug.driver.utils.ResponseParser; + +public class InternalSession implements Session { + + private final Client client; + + public InternalSession(Client client) { + this.client = client; + } + + @Override + public ResultSet run(String query) { + try { + byte[] request = QuerySerializer.serialize(query); + byte[] response = client.syncPost(request); + return ResponseParser.parse(response); + } catch (Exception e) { + throw new RuntimeException("Failed to execute query", e); + } + } + + @Override + public ResultSet run(String query, Map parameters) { + try { + byte[] request = QuerySerializer.serialize(query, parameters); + byte[] response = client.syncPost(request); + return ResponseParser.parse(response); + } catch (Exception e) { + throw new RuntimeException("Failed to execute query", e); + } + } + + @Override + public ResultSet run(String query, AccessMode mode) { + try { + byte[] request = QuerySerializer.serialize(query, mode); + byte[] response = client.syncPost(request); + return ResponseParser.parse(response); + } catch (Exception e) { + throw new RuntimeException("Failed to execute query", e); + } + } + + @Override + public ResultSet run(String query, Map parameters, AccessMode mode) { + try { + byte[] request = QuerySerializer.serialize(query, parameters, mode); + byte[] response = client.syncPost(request); + return ResponseParser.parse(response); + } catch (Exception e) { + throw new RuntimeException("Failed to execute query", e); + } + } + + @Override + public void close() {} + + @Override + public boolean isClosed() { + return false; + } +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java b/tools/java/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java new file mode 100644 index 00000000..599b1f89 --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java @@ -0,0 +1,21 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver.utils; + +public enum AccessMode { + READ, + INSERT, + UPDATE, + SCHEMA, +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/Client.java b/tools/java/src/main/java/org/alibaba/neug/driver/utils/Client.java new file mode 100644 index 00000000..39e7c6df --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/utils/Client.java @@ -0,0 +1,69 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver.utils; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class Client { + + private final String uri; + private static OkHttpClient httpClient = null; + private boolean closed = false; + + public Client(String uri, Config config) { + this.uri = uri; + this.closed = false; + + httpClient = + new OkHttpClient.Builder() + .connectionPool( + new ConnectionPool( + config.getMaxConnectionPoolSize(), + config.getKeepAliveIntervalMillis(), + TimeUnit.MILLISECONDS)) + .retryOnConnectionFailure(true) + .connectTimeout(config.getConnectionTimeoutMillis(), TimeUnit.MILLISECONDS) + .readTimeout(config.getReadTimeoutMillis(), TimeUnit.MILLISECONDS) + .writeTimeout(config.getWriteTimeoutMillis(), TimeUnit.MILLISECONDS) + .build(); + } + + public byte[] syncPost(byte[] request) throws IOException { + RequestBody body = RequestBody.create(request); + Request httpRequest = new Request.Builder().url(uri).post(body).build(); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + return response.body().bytes(); + } + } + + public boolean isClosed() { + return closed; + } + + public void close() { + if (!closed) { + httpClient.connectionPool().evictAll(); + closed = true; + } + } +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/Config.java b/tools/java/src/main/java/org/alibaba/neug/driver/utils/Config.java new file mode 100644 index 00000000..8f4b449f --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/utils/Config.java @@ -0,0 +1,118 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver.utils; + +import java.io.Serializable; + +public final class Config implements Serializable { + private static final long serialVersionUID = 1L; + + public static final class ConfigBuilder { + private int connectionTimeoutMillis = 30000; + private int readTimeoutMillis = 30000; + private int writeTimeoutMillis = 30000; + private int keepAliveIntervalMillis = 30000; + private int maxConnectionPoolSize = 100; + private int maxRequestsPerHost = 1000; + private int maxRequests = 10000; + + public ConfigBuilder withConnectionTimeoutMillis(int connectionTimeoutMillis) { + this.connectionTimeoutMillis = connectionTimeoutMillis; + return this; + } + + public ConfigBuilder withReadTimeoutMillis(int readTimeoutMillis) { + this.readTimeoutMillis = readTimeoutMillis; + return this; + } + + public ConfigBuilder withWriteTimeoutMillis(int writeTimeoutMillis) { + this.writeTimeoutMillis = writeTimeoutMillis; + return this; + } + + public ConfigBuilder withKeepAliveIntervalMillis(int keepAliveIntervalMillis) { + this.keepAliveIntervalMillis = keepAliveIntervalMillis; + return this; + } + + public ConfigBuilder withMaxConnectionPoolSize(int maxConnectionPoolSize) { + this.maxConnectionPoolSize = maxConnectionPoolSize; + return this; + } + + public ConfigBuilder withMaxRequestsPerHost(int maxRequestsPerHost) { + this.maxRequestsPerHost = maxRequestsPerHost; + return this; + } + + public ConfigBuilder withMaxRequests(int maxRequests) { + this.maxRequests = maxRequests; + return this; + } + + public Config build() { + Config config = new Config(); + config.connectionTimeoutMillis = connectionTimeoutMillis; + config.readTimeoutMillis = readTimeoutMillis; + config.writeTimeoutMillis = writeTimeoutMillis; + config.keepAliveIntervalMillis = keepAliveIntervalMillis; + config.maxConnectionPoolSize = maxConnectionPoolSize; + config.maxRequestsPerHost = maxRequestsPerHost; + config.maxRequests = maxRequests; + return config; + } + } + ; + + public static ConfigBuilder builder() { + return new ConfigBuilder(); + } + + public int getConnectionTimeoutMillis() { + return connectionTimeoutMillis; + } + + public int getReadTimeoutMillis() { + return readTimeoutMillis; + } + + public int getWriteTimeoutMillis() { + return writeTimeoutMillis; + } + + public int getKeepAliveIntervalMillis() { + return keepAliveIntervalMillis; + } + + public int getMaxConnectionPoolSize() { + return maxConnectionPoolSize; + } + + public int getMaxRequestsPerHost() { + return maxRequestsPerHost; + } + + public int getMaxRequests() { + return maxRequests; + } + + private int connectionTimeoutMillis; + private int readTimeoutMillis; + private int writeTimeoutMillis; + private int keepAliveIntervalMillis; + private int maxConnectionPoolSize; + private int maxRequestsPerHost; + private int maxRequests; +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java b/tools/java/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java new file mode 100644 index 00000000..b6cece44 --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java @@ -0,0 +1,34 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class JsonUtil { + + private JsonUtil() {} + + private static class Holder { + private static final ObjectMapper INSTANCE = initMapper(); + } + + public static ObjectMapper getInstance() { + return Holder.INSTANCE; + } + + private static ObjectMapper initMapper() { + ObjectMapper mapper = new ObjectMapper(); + return mapper; + } +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java b/tools/java/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java new file mode 100644 index 00000000..8d37d03a --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Map; + +public class QuerySerializer { + + public static byte[] serialize(String query) { + return serialize(query, null, null); + } + + public static byte[] serialize(String query, Map parameters) { + return serialize(query, parameters, null); + } + + public static byte[] serialize(String query, AccessMode accessMode) { + return serialize(query, null, accessMode); + } + + public static byte[] serialize( + String query, Map parameters, AccessMode accessMode) { + try { + ObjectMapper mapper = JsonUtil.getInstance(); + ObjectNode root = mapper.createObjectNode(); + root.put("query", query); + if (parameters != null) { + ObjectNode paramsNode = mapper.createObjectNode(); + for (Map.Entry entry : parameters.entrySet()) { + paramsNode.putPOJO(entry.getKey(), entry.getValue()); + } + root.set("parameters", paramsNode); + } + if (accessMode != null) { + root.put("access_mode", accessMode.name()); + } + return mapper.writeValueAsBytes(root); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize query", e); + } + } +} diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java b/tools/java/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java new file mode 100644 index 00000000..99454637 --- /dev/null +++ b/tools/java/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java @@ -0,0 +1,30 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver.utils; + +import org.alibaba.neug.driver.ResultSet; +import org.alibaba.neug.driver.Results; +import org.alibaba.neug.driver.internal.InternalResultSet; + +public class ResponseParser { + + public static ResultSet parse(byte[] response) { + try { + Results.QueryResponse queryResponse = Results.QueryResponse.parseFrom(response); + return new InternalResultSet(queryResponse); + } catch (Exception e) { + throw new RuntimeException("Failed to parse response", e); + } + } +} From 489863778aa48337a5a86bc8d800f1bb8dcbcac1 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 12 Mar 2026 16:44:53 +0800 Subject: [PATCH 02/60] add test cases --- tools/java/USAGE.md | 186 -------- tools/java_driver/USAGE.md | 112 +++++ tools/{java => java_driver}/pom.xml | 2 +- .../java/org/alibaba/neug/driver/Driver.java | 6 +- .../alibaba/neug/driver/GraphDatabase.java | 21 +- .../org/alibaba/neug/driver/ResultSet.java | 105 ++++- .../java/org/alibaba/neug/driver/Session.java | 6 +- .../neug/driver/internal/InternalDriver.java | 12 + .../driver/internal/InternalResultSet.java | 293 ++++++++++--- .../neug/driver/internal/InternalSession.java | 43 +- .../alibaba/neug/driver/utils/AccessMode.java | 16 + .../org/alibaba/neug/driver/utils/Client.java | 32 +- .../org/alibaba/neug/driver/utils/Config.java | 97 +++-- .../alibaba/neug/driver/utils/JsonUtil.java | 11 + .../neug/driver/utils/QuerySerializer.java | 34 ++ .../neug/driver/utils/ResponseParser.java | 13 + .../alibaba/neug/driver/AccessModeTest.java | 58 +++ .../org/alibaba/neug/driver/ConfigTest.java | 74 ++++ .../neug/driver/GraphDatabaseTest.java | 79 ++++ .../neug/driver/InternalResultSetTest.java | 402 ++++++++++++++++++ .../org/alibaba/neug/driver/JsonUtilTest.java | 48 +++ .../neug/driver/QuerySerializerTest.java | 130 ++++++ 22 files changed, 1469 insertions(+), 311 deletions(-) delete mode 100644 tools/java/USAGE.md create mode 100644 tools/java_driver/USAGE.md rename tools/{java => java_driver}/pom.xml (99%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/Driver.java (92%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/GraphDatabase.java (72%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/ResultSet.java (57%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/Session.java (95%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java (76%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java (60%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java (66%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java (55%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/utils/Client.java (69%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/utils/Config.java (61%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java (75%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java (63%) rename tools/{java => java_driver}/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java (71%) create mode 100644 tools/java_driver/src/test/java/org/alibaba/neug/driver/AccessModeTest.java create mode 100644 tools/java_driver/src/test/java/org/alibaba/neug/driver/ConfigTest.java create mode 100644 tools/java_driver/src/test/java/org/alibaba/neug/driver/GraphDatabaseTest.java create mode 100644 tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java create mode 100644 tools/java_driver/src/test/java/org/alibaba/neug/driver/JsonUtilTest.java create mode 100644 tools/java_driver/src/test/java/org/alibaba/neug/driver/QuerySerializerTest.java diff --git a/tools/java/USAGE.md b/tools/java/USAGE.md deleted file mode 100644 index af054024..00000000 --- a/tools/java/USAGE.md +++ /dev/null @@ -1,186 +0,0 @@ -# NeuG Java Driver 使用指南 - -## 在其他项目中使用 - -### 方式一:本地 Maven 仓库(开发测试) - -1. 安装到本地 Maven 仓库: -```bash -cd /data/0319/neug2/tools/java -mvn clean install -DskipTests -``` - -2. 在其他项目的 `pom.xml` 中添加依赖: -```xml - - org.alibaba.neug - neug-java-driver - 1.0.0-SNAPSHOT - -``` - -### 方式二:使用本地 JAR 文件 - -如果不想使用 Maven 仓库,可以直接引用编译好的 JAR: - -```xml - - org.alibaba.neug - neug-java-driver - 1.0.0-SNAPSHOT - system - /data/0319/neug2/tools/java/target/neug-java-driver-1.0.0-SNAPSHOT.jar - -``` - -### 方式三:发布到私有 Maven 仓库(生产环境推荐) - -1. 在 `pom.xml` 中添加 distributionManagement 配置: -```xml - - - your-releases - Your Release Repository - http://your-nexus-server/repository/maven-releases/ - - - your-snapshots - Your Snapshot Repository - http://your-nexus-server/repository/maven-snapshots/ - - -``` - -2. 在 `~/.m2/settings.xml` 中配置仓库认证信息: -```xml - - - your-releases - your-username - your-password - - - your-snapshots - your-username - your-password - - -``` - -3. 发布到仓库: -```bash -mvn clean deploy -``` - -## 使用示例 - -### 基本连接 - -```java -import org.alibaba.neug.driver.*; - -public class Example { - public static void main(String[] args) { - // 创建驱动 - Driver driver = GraphDatabase.driver("http://localhost:8000"); - - try { - // 验证连接 - driver.verifyConnectivity(); - - // 创建会话 - try (Session session = driver.session()) { - // 执行查询 - try (ResultSet rs = session.run("MATCH (n) RETURN n LIMIT 10")) { - while (rs.next()) { - System.out.println(rs.getObject("n")); - } - } - } - } finally { - driver.close(); - } - } -} -``` - -### 带配置的连接 - -```java -import org.alibaba.neug.driver.*; -import java.util.concurrent.TimeUnit; - -public class ConfigExample { - public static void main(String[] args) { - Config config = Config.builder() - .withConnectionTimeout(30, TimeUnit.SECONDS) - .withMaxRetries(3) - .build(); - - Driver driver = GraphDatabase.driver("http://localhost:8000", config); - - try (Session session = driver.session(AccessMode.READ)) { - // 只读查询 - try (ResultSet rs = session.run("MATCH (n:Person) RETURN n.name, n.age")) { - while (rs.next()) { - String name = rs.getString("n.name"); - int age = rs.getInt("n.age"); - System.out.println(name + ", " + age); - } - } - } finally { - driver.close(); - } - } -} -``` - -### 带参数的查询 - -```java -import java.util.HashMap; -import java.util.Map; - -Map parameters = new HashMap<>(); -parameters.put("name", "Alice"); -parameters.put("age", 30); - -try (Session session = driver.session()) { - String query = "CREATE (p:Person {name: $name, age: $age}) RETURN p"; - try (ResultSet rs = session.run(query, parameters)) { - if (rs.next()) { - System.out.println("Created: " + rs.getObject("p")); - } - } -} -``` - -## Gradle 项目使用 - -如果使用 Gradle,在 `build.gradle` 中添加: - -```groovy -dependencies { - implementation 'org.alibaba.neug:neug-java-driver:1.0.0-SNAPSHOT' -} -``` - -对于本地 JAR: -```groovy -dependencies { - implementation files('/data/0319/neug2/tools/java/target/neug-java-driver-1.0.0-SNAPSHOT.jar') - implementation 'com.squareup.okhttp3:okhttp:4.11.0' - implementation 'com.google.protobuf:protobuf-java:4.29.6' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' -} -``` - -## 依赖说明 - -该 driver 依赖以下库: -- OkHttp 4.11.0 - HTTP 客户端 -- Protocol Buffers 4.29.6 - 序列化 -- Jackson 2.15.2 - JSON 处理 -- SLF4J 2.0.7 - 日志接口 - -这些依赖会被 Maven 自动管理。 diff --git a/tools/java_driver/USAGE.md b/tools/java_driver/USAGE.md new file mode 100644 index 00000000..636dddf1 --- /dev/null +++ b/tools/java_driver/USAGE.md @@ -0,0 +1,112 @@ +# NeuG Java Driver Usage Guide + +## Using in Other Projects + + +1. Install to local Maven repository: +```bash +cd tools/java +mvn clean install -DskipTests +``` + +2. Add dependency to your project's `pom.xml`: +```xml + + org.alibaba.neug + neug-java-driver + 1.0.0-SNAPSHOT + +``` + + +## Usage Examples + +### Basic Connection + +```java +import org.alibaba.neug.driver.*; + +public class Example { + public static void main(String[] args) { + // Create driver + Driver driver = GraphDatabase.driver("http://localhost:10000"); + + try { + // Verify connectivity + driver.verifyConnectivity(); + + // Create session + try (Session session = driver.session()) { + // Execute query + try (ResultSet rs = session.run("MATCH (n) RETURN n LIMIT 10")) { + while (rs.next()) { + System.out.println(rs.getObject("n")); + } + } + } + } finally { + driver.close(); + } + } +} +``` + +### Connection with Configuration + +```java +import org.alibaba.neug.driver.*; +import org.alibaba.neug.driver.utils.*; + +public class ConfigExample { + public static void main(String[] args) { + Config config = Config.builder() + .withConnectionTimeout(3000) + .build(); + + Driver driver = GraphDatabase.driver("http://localhost:10000", config); + + try (Session session = driver.session(AccessMode.READ)) { + // Read-only query + try (ResultSet rs = session.run("MATCH (n:Person) RETURN n.name, n.age")) { + while (rs.next()) { + String name = rs.getString("n.name"); + int age = rs.getInt("n.age"); + System.out.println(name + ", " + age); + } + } + } finally { + driver.close(); + } + } +} +``` + +### Parameterized Query + +```java +import java.util.HashMap; +import java.util.Map; + +Map parameters = new HashMap<>(); +parameters.put("name", "Alice"); +parameters.put("age", 30); + +try (Session session = driver.session()) { + String query = "CREATE (p:Person {name: $name, age: $age}) RETURN p"; + try (ResultSet rs = session.run(query, parameters)) { + if (rs.next()) { + System.out.println("Created: " + rs.getObject("p")); + } + } +} +``` + +## Dependencies + +This driver depends on the following libraries: +- OkHttp 4.11.0 - HTTP client +- Protocol Buffers 4.29.6 - Serialization +- Jackson 2.15.2 - JSON processing +- SLF4J 2.0.7 - Logging interface + +These dependencies are automatically managed by Maven. diff --git a/tools/java/pom.xml b/tools/java_driver/pom.xml similarity index 99% rename from tools/java/pom.xml rename to tools/java_driver/pom.xml index 8edeaba5..861a846b 100644 --- a/tools/java/pom.xml +++ b/tools/java_driver/pom.xml @@ -41,7 +41,7 @@ com.squareup.okhttp3 okhttp - 4.9.0 + 4.11.0 diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/Driver.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/Driver.java similarity index 92% rename from tools/java/src/main/java/org/alibaba/neug/driver/Driver.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/Driver.java index 0cc6019e..89fc7a3a 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/Driver.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/Driver.java @@ -18,13 +18,13 @@ /** * The main driver interface for NeuG database connections. * - *

A driver manages database connections and provides sessions for executing queries. - * It is responsible for connection pooling, resource management, and cleanup. + *

A driver manages database connections and provides sessions for executing queries. It is + * responsible for connection pooling, resource management, and cleanup. * *

Example usage: * *

{@code
- * Driver driver = GraphDatabase.driver("http://localhost:8080");
+ * Driver driver = GraphDatabase.driver("http://localhost:10000");
  * try (Session session = driver.session()) {
  *     ResultSet results = session.run("MATCH (n) RETURN n");
  *     // Process results
diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/GraphDatabase.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/GraphDatabase.java
similarity index 72%
rename from tools/java/src/main/java/org/alibaba/neug/driver/GraphDatabase.java
rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/GraphDatabase.java
index 63c4dcfd..3217b945 100644
--- a/tools/java/src/main/java/org/alibaba/neug/driver/GraphDatabase.java
+++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/GraphDatabase.java
@@ -26,13 +26,13 @@
  *
  * 
{@code
  * // Create a driver with default configuration
- * Driver driver = GraphDatabase.driver("http://localhost:8080");
+ * Driver driver = GraphDatabase.driver("http://localhost:10000");
  *
  * // Create a driver with custom configuration
  * Config config = Config.builder()
  *     .withMaxConnectionPoolSize(10)
  *     .build();
- * Driver driver = GraphDatabase.driver("http://localhost:8080", config);
+ * Driver driver = GraphDatabase.driver("http://localhost:10000", config);
  * }
*/ public final class GraphDatabase { @@ -44,23 +44,36 @@ private GraphDatabase() { /** * Creates a new driver instance with default configuration. * - * @param uri the URI of the NeuG database server (e.g., "http://localhost:8080") + * @param uri the URI of the NeuG database server (e.g., "http://localhost:10000") * @return a new {@link Driver} instance * @throws IllegalArgumentException if the URI is null or invalid */ public static Driver driver(String uri) { + if (uri == null || uri.isEmpty()) { + throw new IllegalArgumentException("URI cannot be null or empty"); + } + if (!uri.startsWith("http://") && !uri.startsWith("https://")) { + throw new IllegalArgumentException("URI must start with http:// or https://"); + } return new InternalDriver(uri, Config.builder().build()); } /** * Creates a new driver instance with custom configuration. * - * @param uri the URI of the NeuG database server (e.g., "http://localhost:8080") + * @param uri the URI of the NeuG database server (e.g., "http://localhost:10000") * @param config the configuration settings for the driver * @return a new {@link Driver} instance * @throws IllegalArgumentException if the URI or config is null or invalid */ public static Driver driver(String uri, Config config) { + if (uri == null || uri.isEmpty()) { + throw new IllegalArgumentException("URI cannot be null or empty"); + } + if (!uri.startsWith("http://") && !uri.startsWith("https://")) { + throw new IllegalArgumentException("URI must start with http:// or https://"); + } + return new InternalDriver(uri, config); } } diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/ResultSet.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java similarity index 57% rename from tools/java/src/main/java/org/alibaba/neug/driver/ResultSet.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java index 74c655d3..a920d756 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/ResultSet.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java @@ -13,6 +13,7 @@ */ package org.alibaba.neug.driver; +import java.math.BigDecimal; import java.sql.Date; import java.sql.Timestamp; import java.util.List; @@ -26,10 +27,10 @@ *

Example usage: * *

{@code
- * ResultSet results = session.run("MATCH (n:Person) RETURN n.name, n.age");
+ * ResultSet results = session.run("MATCH (n:Person) RETURN n.name as name, n.age as age");
  * while (results.next()) {
- *     String name = results.getString("n.name");
- *     int age = results.getInt("n.age");
+ *     String name = results.getString("name");
+ *     int age = results.getInt("age");
  *     System.out.println(name + " is " + age + " years old");
  * }
  * results.close();
@@ -79,6 +80,7 @@ public interface ResultSet extends AutoCloseable {
      *
      * @param columnName the name of the column
      * @return the column value
+     * @throws IllegalArgumentException if the column name is not valid
      */
     Object getObject(String columnName);
 
@@ -87,137 +89,234 @@ public interface ResultSet extends AutoCloseable {
      *
      * @param columnIndex the column index (0-based)
      * @return the column value
+     * @throws IndexOutOfBoundsException if the column index is out of bounds
      */
     Object getObject(int columnIndex);
 
     /**
      * Retrieves the value of the designated column as an int.
      *
+     * 

Type requirement: The column must be of type INT32 or compatible numeric type. + * * @param columnName the name of the column * @return the column value; 0 if the value is SQL NULL + * @throws IllegalArgumentException if the column name is not valid + * @throws ClassCastException if the column is not of a compatible type */ int getInt(String columnName); /** * Retrieves the value of the designated column as an int. * + *

Type requirement: The column must be of type INT32 or compatible numeric type. + * * @param columnIndex the column index (0-based) * @return the column value; 0 if the value is SQL NULL + * @throws IndexOutOfBoundsException if the column index is out of bounds + * @throws ClassCastException if the column is not of a compatible type */ int getInt(int columnIndex); /** * Retrieves the value of the designated column as a long. * + *

Type requirement: The column must be of type INT64 or compatible numeric type. + * * @param columnName the name of the column * @return the column value; 0 if the value is SQL NULL + * @throws IllegalArgumentException if the column name is not valid + * @throws ClassCastException if the column is not of a compatible type */ long getLong(String columnName); /** * Retrieves the value of the designated column as a long. * + *

Type requirement: The column must be of type INT64 or compatible numeric type. + * * @param columnIndex the column index (0-based) * @return the column value; 0 if the value is SQL NULL + * @throws IndexOutOfBoundsException if the column index is out of bounds + * @throws ClassCastException if the column is not of a compatible type */ long getLong(int columnIndex); /** * Retrieves the value of the designated column as a String. * + *

Type requirement: The column must be of type STRING or a type that can be converted + * to string. + * * @param columnName the name of the column * @return the column value; {@code null} if the value is SQL NULL + * @throws IllegalArgumentException if the column name is not valid + * @throws ClassCastException if the column is not of a compatible type */ String getString(String columnName); /** * Retrieves the value of the designated column as a String. * + *

Type requirement: The column must be of type STRING or a type that can be converted + * to string. + * * @param columnIndex the column index (0-based) * @return the column value; {@code null} if the value is SQL NULL + * @throws IndexOutOfBoundsException if the column index is out of bounds + * @throws ClassCastException if the column is not of a compatible type */ String getString(int columnIndex); /** * Retrieves the value of the designated column as a Date. * + *

Type requirement: The column must be of type DATE. + * * @param columnName the name of the column * @return the column value; {@code null} if the value is SQL NULL + * @throws IllegalArgumentException if the column name is not valid + * @throws ClassCastException if the column is not of type DATE */ Date getDate(String columnName); /** * Retrieves the value of the designated column as a Date. * + *

Type requirement: The column must be of type DATE. + * * @param columnIndex the column index (0-based) * @return the column value; {@code null} if the value is SQL NULL + * @throws IndexOutOfBoundsException if the column index is out of bounds + * @throws ClassCastException if the column is not of type DATE */ Date getDate(int columnIndex); /** * Retrieves the value of the designated column as a Timestamp. * + *

Type requirement: The column must be of type TIMESTAMP. + * * @param columnName the name of the column * @return the column value; {@code null} if the value is SQL NULL + * @throws IllegalArgumentException if the column name is not valid + * @throws ClassCastException if the column is not of type TIMESTAMP */ Timestamp getTimestamp(String columnName); /** * Retrieves the value of the designated column as a Timestamp. * + *

Type requirement: The column must be of type TIMESTAMP. + * * @param columnIndex the column index (0-based) * @return the column value; {@code null} if the value is SQL NULL + * @throws IndexOutOfBoundsException if the column index is out of bounds + * @throws ClassCastException if the column is not of type TIMESTAMP */ Timestamp getTimestamp(int columnIndex); /** * Retrieves the value of the designated column as a boolean. * + *

Type requirement: The column must be of type BOOLEAN. + * * @param columnName the name of the column * @return the column value; {@code false} if the value is SQL NULL + * @throws IllegalArgumentException if the column name is not valid + * @throws ClassCastException if the column is not of type BOOLEAN */ boolean getBoolean(String columnName); /** * Retrieves the value of the designated column as a boolean. * + *

Type requirement: The column must be of type BOOLEAN. + * * @param columnIndex the column index (0-based) * @return the column value; {@code false} if the value is SQL NULL + * @throws IndexOutOfBoundsException if the column index is out of bounds + * @throws ClassCastException if the column is not of type BOOLEAN */ boolean getBoolean(int columnIndex); /** * Retrieves the value of the designated column as a double. * + *

Type requirement: The column must be of type DOUBLE or compatible numeric type. + * * @param columnName the name of the column * @return the column value; 0 if the value is SQL NULL + * @throws IllegalArgumentException if the column name is not valid + * @throws ClassCastException if the column is not of a compatible type */ double getDouble(String columnName); /** * Retrieves the value of the designated column as a double. * + *

Type requirement: The column must be of type DOUBLE or compatible numeric type. + * * @param columnIndex the column index (0-based) * @return the column value; 0 if the value is SQL NULL + * @throws IndexOutOfBoundsException if the column index is out of bounds + * @throws ClassCastException if the column is not of a compatible type */ double getDouble(int columnIndex); /** * Retrieves the value of the designated column as a float. * + *

Type requirement: The column must be of type FLOAT or compatible numeric type. + * * @param columnName the name of the column * @return the column value; 0 if the value is SQL NULL + * @throws IllegalArgumentException if the column name is not valid + * @throws ClassCastException if the column is not of a compatible type */ float getFloat(String columnName); /** * Retrieves the value of the designated column as a float. * + *

Type requirement: The column must be of type FLOAT or compatible numeric type. + * * @param columnIndex the column index (0-based) * @return the column value; 0 if the value is SQL NULL + * @throws IndexOutOfBoundsException if the column index is out of bounds + * @throws ClassCastException if the column is not of a compatible type */ float getFloat(int columnIndex); + /** + * Retrieves the value of the designated column as a BigDecimal. + * + *

Type requirement: The column must be a numeric type (INT32, INT64, FLOAT, DOUBLE). + * + *

BigDecimal provides arbitrary precision and is ideal for financial calculations or when + * precision is critical. + * + * @param columnName the name of the column + * @return the column value; {@code null} if the value is SQL NULL + * @throws IllegalArgumentException if the column name is not valid + * @throws ClassCastException if the column is not a numeric type + */ + BigDecimal getBigDecimal(String columnName); + + /** + * Retrieves the value of the designated column as a BigDecimal. + * + *

Type requirement: The column must be a numeric type (INT32, INT64, FLOAT, DOUBLE). + * + *

BigDecimal provides arbitrary precision and is ideal for financial calculations or when + * precision is critical. + * + * @param columnIndex the column index (0-based) + * @return the column value; {@code null} if the value is SQL NULL + * @throws IndexOutOfBoundsException if the column index is out of bounds + * @throws ClassCastException if the column is not a numeric type + */ + BigDecimal getBigDecimal(int columnIndex); + /** * Reports whether the last column read had a value of SQL NULL. * diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/Session.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/Session.java similarity index 95% rename from tools/java/src/main/java/org/alibaba/neug/driver/Session.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/Session.java index b60f27f8..de6ff418 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/Session.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/Session.java @@ -29,7 +29,7 @@ * ResultSet results = session.run("MATCH (n:Person) WHERE n.age > $age RETURN n", * Map.of("age", 30)); * while (results.next()) { - * System.out.println(results.getString("n")); + * System.out.println(results.getObject("n").toString()); * } * } * }

@@ -75,9 +75,7 @@ public interface Session extends AutoCloseable { */ ResultSet run(String statement, Map parameters, AccessMode mode); - /** - * Closes the session and releases all associated resources. - */ + /** Closes the session and releases all associated resources. */ @Override void close(); diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java similarity index 76% rename from tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java index d10d185c..9bc0007b 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java @@ -19,10 +19,22 @@ import org.alibaba.neug.driver.utils.Client; import org.alibaba.neug.driver.utils.Config; +/** + * Internal implementation of the {@link Driver} interface. + * + *

This class manages the lifecycle of database connections and provides session creation + * capabilities. It uses an HTTP client to communicate with the NeuG database server. + */ public class InternalDriver implements Driver { private static Client client = null; + /** + * Constructs a new InternalDriver with the specified URI and configuration. + * + * @param uri the URI of the database server + * @param config the configuration for the driver + */ public InternalDriver(String uri, Config config) { client = new Client(uri, config); } diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java similarity index 60% rename from tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java index a580ff75..7fd9e781 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java @@ -16,6 +16,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.protobuf.ByteString; +import java.math.BigDecimal; +import java.math.BigInteger; import java.sql.Date; import java.sql.Timestamp; import java.util.ArrayList; @@ -25,11 +27,26 @@ import org.alibaba.neug.driver.Results; import org.alibaba.neug.driver.utils.JsonUtil; +/** + * Internal implementation of the {@link ResultSet} interface. + * + *

This class provides access to query results returned from the database server. It wraps a + * Protocol Buffers QueryResponse object and provides methods to navigate through rows and extract + * column values in various data types. + * + *

The ResultSet maintains a cursor position and supports both forward and backward navigation, + * as well as absolute and relative positioning. + */ public class InternalResultSet implements ResultSet { + /** + * Constructs a new InternalResultSet from a Protocol Buffers query response. + * + * @param response the query response from the database server + */ public InternalResultSet(Results.QueryResponse response) { this.response = response; this.currentIndex = -1; - this.is_null = false; + this.was_null = false; this.closed = false; } @@ -79,6 +96,7 @@ public Object getObject(String columnName) { @Override public Object getObject(int columnIndex) { // Return the appropriate type based on the array type + checkIndex(columnIndex); Results.Array array = response.getArrays(columnIndex); try { return getObject(array, currentIndex, false); @@ -94,7 +112,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { if (!isNullSetted) { ByteString nullBitmap = array.getStringArray().getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -105,7 +123,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { if (!isNullSetted) { ByteString nullBitmap = array.getInt32Array().getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -116,7 +134,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { if (!isNullSetted) { ByteString nullBitmap = array.getInt64Array().getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -127,7 +145,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { if (!isNullSetted) { ByteString nullBitmap = array.getBoolArray().getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -138,7 +156,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { if (!isNullSetted) { ByteString nullBitmap = array.getDoubleArray().getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -149,7 +167,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { if (!isNullSetted) { ByteString nullBitmap = array.getTimestampArray().getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -160,7 +178,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { if (!isNullSetted) { ByteString nullBitmap = array.getDateArray().getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -173,7 +191,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted if (!isNullSetted) { ByteString nullBitmap = listArray.getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -193,7 +211,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted if (!isNullSetted) { ByteString nullBitmap = structArray.getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -210,7 +228,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted ObjectMapper mapper = JsonUtil.getInstance(); if (!isNullSetted) { ByteString nullBitmap = vertexArray.getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -227,7 +245,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted ObjectMapper mapper = JsonUtil.getInstance(); if (!isNullSetted) { ByteString nullBitmap = edgeArray.getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -244,7 +262,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted ObjectMapper mapper = JsonUtil.getInstance(); if (!isNullSetted) { ByteString nullBitmap = pathArray.getValidity(); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; @@ -255,8 +273,48 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted new TypeReference>() {}); return map; } + case INTERVAL_ARRAY: + { + Results.IntervalArray intervalArray = array.getIntervalArray(); + ObjectMapper mapper = JsonUtil.getInstance(); + if (!isNullSetted) { + ByteString nullBitmap = intervalArray.getValidity(); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + return intervalArray.getValues(rowIndex); + } + case UINT32_ARRAY: + { + Results.UInt32Array uint32Array = array.getUint32Array(); + if (!isNullSetted) { + ByteString nullBitmap = uint32Array.getValidity(); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + // Convert uint32 to long to avoid overflow + return Integer.toUnsignedLong(uint32Array.getValues(rowIndex)); + } + case UINT64_ARRAY: + { + Results.UInt64Array uint64Array = array.getUint64Array(); + if (!isNullSetted) { + ByteString nullBitmap = uint64Array.getValidity(); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + // Convert uint64 to BigInteger to avoid overflow + long value = uint64Array.getValues(rowIndex); + return new BigInteger(Long.toUnsignedString(value)); + } default: - throw new IllegalArgumentException( + throw new UnsupportedOperationException( "Unsupported array type: " + array.getTypedArrayCase()); } } @@ -269,17 +327,18 @@ public int getInt(String columnName) { @Override public int getInt(int columnIndex) { + checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); - if (!arr.hasInt32Array()) { - throw new IllegalArgumentException("Column " + columnIndex + " is not of type int32"); + if(arr.hasInt32Array()) { + Results.Int32Array array = arr.getInt32Array(); + ByteString nullBitmap = array.getValidity(); + int value = array.getValues(currentIndex); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return value; } - Results.Int32Array array = arr.getInt32Array(); - ByteString nullBitmap = array.getValidity(); - int value = array.getValues(currentIndex); - is_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; - return value; + return getNumericValue(arr).intValue(); } @Override @@ -290,17 +349,18 @@ public long getLong(String columnName) { @Override public long getLong(int columnIndex) { + checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); - if (!arr.hasInt64Array()) { - throw new IllegalArgumentException("Column " + columnIndex + " is not of type int64"); + if(arr.hasInt64Array()) { + Results.Int64Array array = arr.getInt64Array(); + ByteString nullBitmap = array.getValidity(); + long value = array.getValues(currentIndex); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return value; } - Results.Int64Array array = arr.getInt64Array(); - ByteString nullBitmap = array.getValidity(); - long value = array.getValues(currentIndex); - is_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; - return value; + return getNumericValue(arr).longValue(); } @Override @@ -311,14 +371,15 @@ public String getString(String columnName) { @Override public String getString(int columnIndex) { + checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (!arr.hasStringArray()) { - throw new IllegalArgumentException("Column " + columnIndex + " is not of type string"); + return getObject(columnIndex).toString(); } Results.StringArray array = arr.getStringArray(); ByteString nullBitmap = array.getValidity(); String value = array.getValues(currentIndex); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; return value; @@ -332,14 +393,15 @@ public Date getDate(String columnName) { @Override public Date getDate(int columnIndex) { + checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (!arr.hasDateArray()) { - throw new IllegalArgumentException("Column " + columnIndex + " is not of type date"); + throw new ClassCastException("Column " + columnIndex + " is not of type date"); } Results.DateArray array = arr.getDateArray(); ByteString nullBitmap = array.getValidity(); long timestamp = array.getValues(currentIndex); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; return new Date(timestamp); @@ -353,15 +415,15 @@ public Timestamp getTimestamp(String columnName) { @Override public Timestamp getTimestamp(int columnIndex) { + checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (!arr.hasTimestampArray()) { - throw new IllegalArgumentException( - "Column " + columnIndex + " is not of type timestamp"); + throw new ClassCastException("Column " + columnIndex + " is not of type timestamp"); } Results.TimestampArray array = arr.getTimestampArray(); ByteString nullBitmap = array.getValidity(); long timestamp = array.getValues(currentIndex); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; return new Timestamp(timestamp); @@ -375,14 +437,15 @@ public boolean getBoolean(String columnName) { @Override public boolean getBoolean(int columnIndex) { + checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (!arr.hasBoolArray()) { - throw new IllegalArgumentException("Column " + columnIndex + " is not of type boolean"); + throw new ClassCastException("Column " + columnIndex + " is not of type boolean"); } Results.BoolArray array = arr.getBoolArray(); ByteString nullBitmap = array.getValidity(); boolean value = array.getValues(currentIndex); - is_null = + was_null = !nullBitmap.isEmpty() && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; return value; @@ -396,17 +459,18 @@ public double getDouble(String columnName) { @Override public double getDouble(int columnIndex) { + checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); - if (!arr.hasDoubleArray()) { - throw new IllegalArgumentException("Column " + columnIndex + " is not of type double"); + if(arr.hasFloatArray()) { + Results.FloatArray array = arr.getFloatArray(); + ByteString nullBitmap = array.getValidity(); + float value = array.getValues(currentIndex); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return value; } - Results.DoubleArray array = arr.getDoubleArray(); - ByteString nullBitmap = array.getValidity(); - double value = array.getValues(currentIndex); - is_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; - return value; + return getNumericValue(arr).doubleValue(); } @Override @@ -417,22 +481,48 @@ public float getFloat(String columnName) { @Override public float getFloat(int columnIndex) { + checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); - if (!arr.hasFloatArray()) { - throw new IllegalArgumentException("Column " + columnIndex + " is not of type float"); + if(arr.hasFloatArray()) { + Results.FloatArray array = arr.getFloatArray(); + ByteString nullBitmap = array.getValidity(); + float value = array.getValues(currentIndex); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + return value; } - Results.FloatArray array = arr.getFloatArray(); - ByteString nullBitmap = array.getValidity(); - float value = array.getValues(currentIndex); - is_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; - return value; + return getNumericValue(arr).floatValue(); + } + + @Override + public BigDecimal getBigDecimal(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getBigDecimal(columnIndex); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) { + checkIndex(columnIndex); + Results.Array arr = response.getArrays(columnIndex); + Number value = getNumericValue(arr); + if (value == null) { + return null; + } + if (value instanceof BigInteger) { + return new BigDecimal((BigInteger) value); + } else if (value instanceof Integer || value instanceof Long) { + return new BigDecimal(value.longValue()); + } else if (value instanceof Float || value instanceof Double) { + return BigDecimal.valueOf(value.doubleValue()); + } + throw new ClassCastException( + "Column " + columnIndex + " cannot be converted to BigDecimal"); } @Override public boolean wasNull() { - return is_null; + return was_null; } @Override @@ -466,8 +556,89 @@ private int getColumnIndex(String columnName) { throw new IllegalArgumentException("Column not found: " + columnName); } + private void checkIndex(int columnIndex) { + if (columnIndex < 0 || columnIndex >= response.getArraysCount()) { + throw new IndexOutOfBoundsException("Invalid column index: " + columnIndex); + } + } + + /** + * Generic method to extract numeric value from any numeric array type. + * + * @param arr the array to extract from + * @return the numeric value as a Number object + */ + private Number getNumericValue(Results.Array arr) { + ByteString nullBitmap; + + if (arr.hasInt32Array()) { + Results.Int32Array array = arr.getInt32Array(); + nullBitmap = array.getValidity(); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) + == 0; + return array.getValues(currentIndex); + } + + if (arr.hasInt64Array()) { + Results.Int64Array array = arr.getInt64Array(); + nullBitmap = array.getValidity(); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) + == 0; + return array.getValues(currentIndex); + } + + if (arr.hasFloatArray()) { + Results.FloatArray array = arr.getFloatArray(); + nullBitmap = array.getValidity(); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) + == 0; + return array.getValues(currentIndex); + } + + if (arr.hasDoubleArray()) { + Results.DoubleArray array = arr.getDoubleArray(); + nullBitmap = array.getValidity(); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) + == 0; + return array.getValues(currentIndex); + } + + if (arr.hasUint32Array()) { + Results.UInt32Array array = arr.getUint32Array(); + nullBitmap = array.getValidity(); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) + == 0; + // Convert unsigned int32 to signed long to avoid overflow + return Integer.toUnsignedLong(array.getValues(currentIndex)); + } + + if (arr.hasUint64Array()) { + Results.UInt64Array array = arr.getUint64Array(); + nullBitmap = array.getValidity(); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) + == 0; + // Convert unsigned int64 to BigInteger to avoid overflow + long value = array.getValues(currentIndex); + return new BigInteger(Long.toUnsignedString(value)); + } + + throw new ClassCastException("Column is not a numeric type"); + } + private Results.QueryResponse response; private int currentIndex; - private boolean is_null; + private boolean was_null; private boolean closed; } diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java similarity index 66% rename from tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java index 04b70ead..5f8f72e8 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java @@ -21,45 +21,40 @@ import org.alibaba.neug.driver.utils.QuerySerializer; import org.alibaba.neug.driver.utils.ResponseParser; +/** + * Internal implementation of the {@link Session} interface. + * + *

This class handles query execution by serializing queries, sending them to the database server + * via HTTP, and parsing the responses into ResultSet objects. + */ public class InternalSession implements Session { private final Client client; + private boolean closed; + /** + * Constructs a new InternalSession with the specified client. + * + * @param client the HTTP client used to communicate with the database + */ public InternalSession(Client client) { this.client = client; + this.closed = false; } @Override public ResultSet run(String query) { - try { - byte[] request = QuerySerializer.serialize(query); - byte[] response = client.syncPost(request); - return ResponseParser.parse(response); - } catch (Exception e) { - throw new RuntimeException("Failed to execute query", e); - } + return run(query, null, null); } @Override public ResultSet run(String query, Map parameters) { - try { - byte[] request = QuerySerializer.serialize(query, parameters); - byte[] response = client.syncPost(request); - return ResponseParser.parse(response); - } catch (Exception e) { - throw new RuntimeException("Failed to execute query", e); - } + return run(query, parameters, null); } @Override public ResultSet run(String query, AccessMode mode) { - try { - byte[] request = QuerySerializer.serialize(query, mode); - byte[] response = client.syncPost(request); - return ResponseParser.parse(response); - } catch (Exception e) { - throw new RuntimeException("Failed to execute query", e); - } + return run(query, null, mode); } @Override @@ -74,10 +69,12 @@ public ResultSet run(String query, Map parameters, AccessMode mo } @Override - public void close() {} + public void close() { + closed = true; + } @Override public boolean isClosed() { - return false; + return closed; } } diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java similarity index 55% rename from tools/java/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java index 599b1f89..9f52356b 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java @@ -13,9 +13,25 @@ */ package org.alibaba.neug.driver.utils; +/** + * Enumeration of access modes for database operations. + * + *

The access mode indicates the type of operation being performed on the database: + * + *

    + *
  • {@link #READ} - Read-only operations (queries) + *
  • {@link #INSERT} - Insert operations + *
  • {@link #UPDATE} - Update operations + *
  • {@link #SCHEMA} - Schema modification operations + *
+ */ public enum AccessMode { + /** Read-only access mode for query operations. */ READ, + /** Insert access mode for adding new data. */ INSERT, + /** Update access mode for modifying existing data. */ UPDATE, + /** Schema access mode for DDL operations. */ SCHEMA, } diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/Client.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Client.java similarity index 69% rename from tools/java/src/main/java/org/alibaba/neug/driver/utils/Client.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Client.java index 39e7c6df..3977a9ca 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/utils/Client.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Client.java @@ -21,14 +21,26 @@ import okhttp3.RequestBody; import okhttp3.Response; +/** + * HTTP client for communicating with the NeuG database server. + * + *

This class manages HTTP connections using OkHttp and provides synchronous POST operations for + * sending query requests to the database server. + */ public class Client { private final String uri; private static OkHttpClient httpClient = null; private boolean closed = false; + /** + * Constructs a new Client with the specified URI and configuration. + * + * @param uri the URI of the database server + * @param config the configuration for connection pooling and timeouts + */ public Client(String uri, Config config) { - this.uri = uri; + this.uri = uri + "/cypher"; this.closed = false; httpClient = @@ -45,6 +57,13 @@ public Client(String uri, Config config) { .build(); } + /** + * Sends a synchronous POST request to the database server. + * + * @param request the request body as a byte array + * @return the response body as a byte array + * @throws IOException if an I/O error occurs during the request + */ public byte[] syncPost(byte[] request) throws IOException { RequestBody body = RequestBody.create(request); Request httpRequest = new Request.Builder().url(uri).post(body).build(); @@ -56,10 +75,21 @@ public byte[] syncPost(byte[] request) throws IOException { } } + /** + * Checks whether this client has been closed. + * + * @return {@code true} if the client is closed, {@code false} otherwise + */ public boolean isClosed() { return closed; } + /** + * Closes this client and releases all associated resources. + * + *

This method evicts all connections from the connection pool and marks the client as + * closed. + */ public void close() { if (!closed) { httpClient.connectionPool().evictAll(); diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/Config.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Config.java similarity index 61% rename from tools/java/src/main/java/org/alibaba/neug/driver/utils/Config.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Config.java index 8f4b449f..15b05956 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/utils/Config.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Config.java @@ -15,53 +15,83 @@ import java.io.Serializable; +/** + * Configuration for the NeuG driver. + * + *

This class holds various timeout and connection pool settings. Use the {@link ConfigBuilder} + * to create instances. + */ public final class Config implements Serializable { private static final long serialVersionUID = 1L; + /** Builder for creating {@link Config} instances with custom settings. */ public static final class ConfigBuilder { private int connectionTimeoutMillis = 30000; private int readTimeoutMillis = 30000; private int writeTimeoutMillis = 30000; private int keepAliveIntervalMillis = 30000; private int maxConnectionPoolSize = 100; - private int maxRequestsPerHost = 1000; - private int maxRequests = 10000; + /** + * Sets the connection timeout in milliseconds. + * + * @param connectionTimeoutMillis the connection timeout + * @return this builder + */ public ConfigBuilder withConnectionTimeoutMillis(int connectionTimeoutMillis) { this.connectionTimeoutMillis = connectionTimeoutMillis; return this; } + /** + * Sets the read timeout in milliseconds. + * + * @param readTimeoutMillis the read timeout + * @return this builder + */ public ConfigBuilder withReadTimeoutMillis(int readTimeoutMillis) { this.readTimeoutMillis = readTimeoutMillis; return this; } + /** + * Sets the write timeout in milliseconds. + * + * @param writeTimeoutMillis the write timeout + * @return this builder + */ public ConfigBuilder withWriteTimeoutMillis(int writeTimeoutMillis) { this.writeTimeoutMillis = writeTimeoutMillis; return this; } + /** + * Sets the keep-alive interval in milliseconds. + * + * @param keepAliveIntervalMillis the keep-alive interval + * @return this builder + */ public ConfigBuilder withKeepAliveIntervalMillis(int keepAliveIntervalMillis) { this.keepAliveIntervalMillis = keepAliveIntervalMillis; return this; } + /** + * Sets the maximum connection pool size. + * + * @param maxConnectionPoolSize the maximum number of connections in the pool + * @return this builder + */ public ConfigBuilder withMaxConnectionPoolSize(int maxConnectionPoolSize) { this.maxConnectionPoolSize = maxConnectionPoolSize; return this; } - public ConfigBuilder withMaxRequestsPerHost(int maxRequestsPerHost) { - this.maxRequestsPerHost = maxRequestsPerHost; - return this; - } - - public ConfigBuilder withMaxRequests(int maxRequests) { - this.maxRequests = maxRequests; - return this; - } - + /** + * Builds a new {@link Config} instance with the configured settings. + * + * @return a new Config instance + */ public Config build() { Config config = new Config(); config.connectionTimeoutMillis = connectionTimeoutMillis; @@ -69,50 +99,67 @@ public Config build() { config.writeTimeoutMillis = writeTimeoutMillis; config.keepAliveIntervalMillis = keepAliveIntervalMillis; config.maxConnectionPoolSize = maxConnectionPoolSize; - config.maxRequestsPerHost = maxRequestsPerHost; - config.maxRequests = maxRequests; return config; } } - ; + /** + * Creates a new ConfigBuilder for constructing Config instances. + * + * @return a new ConfigBuilder + */ public static ConfigBuilder builder() { return new ConfigBuilder(); } + /** + * Gets the connection timeout in milliseconds. + * + * @return the connection timeout + */ public int getConnectionTimeoutMillis() { return connectionTimeoutMillis; } + /** + * Gets the read timeout in milliseconds. + * + * @return the read timeout + */ public int getReadTimeoutMillis() { return readTimeoutMillis; } + /** + * Gets the write timeout in milliseconds. + * + * @return the write timeout + */ public int getWriteTimeoutMillis() { return writeTimeoutMillis; } + /** + * Gets the keep-alive interval in milliseconds. + * + * @return the keep-alive interval + */ public int getKeepAliveIntervalMillis() { return keepAliveIntervalMillis; } + /** + * Gets the maximum connection pool size. + * + * @return the maximum connection pool size + */ public int getMaxConnectionPoolSize() { return maxConnectionPoolSize; } - public int getMaxRequestsPerHost() { - return maxRequestsPerHost; - } - - public int getMaxRequests() { - return maxRequests; - } - private int connectionTimeoutMillis; private int readTimeoutMillis; private int writeTimeoutMillis; private int keepAliveIntervalMillis; private int maxConnectionPoolSize; - private int maxRequestsPerHost; - private int maxRequests; } diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java similarity index 75% rename from tools/java/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java index b6cece44..8781f91a 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java @@ -15,6 +15,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; +/** + * Utility class providing a shared Jackson ObjectMapper instance. + * + *

This class uses the singleton pattern to ensure a single ObjectMapper instance is reused + * throughout the application, improving performance. + */ public class JsonUtil { private JsonUtil() {} @@ -23,6 +29,11 @@ private static class Holder { private static final ObjectMapper INSTANCE = initMapper(); } + /** + * Gets the singleton ObjectMapper instance. + * + * @return the shared ObjectMapper instance + */ public static ObjectMapper getInstance() { return Holder.INSTANCE; } diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java similarity index 63% rename from tools/java/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java index 8d37d03a..76bce478 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java @@ -17,20 +17,54 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.Map; +/** + * Utility class for serializing queries to JSON format for transmission to the database server. + * + *

This class converts query strings, parameters, and access modes into JSON bytes that can be + * sent over HTTP. + */ public class QuerySerializer { + /** + * Serializes a query without parameters or access mode. + * + * @param query the query string + * @return the serialized query as a byte array + */ public static byte[] serialize(String query) { return serialize(query, null, null); } + /** + * Serializes a query with parameters but without access mode. + * + * @param query the query string + * @param parameters the query parameters as a map of name-value pairs + * @return the serialized query as a byte array + */ public static byte[] serialize(String query, Map parameters) { return serialize(query, parameters, null); } + /** + * Serializes a query with access mode but without parameters. + * + * @param query the query string + * @param accessMode the access mode for the query + * @return the serialized query as a byte array + */ public static byte[] serialize(String query, AccessMode accessMode) { return serialize(query, null, accessMode); } + /** + * Serializes a query with parameters and access mode. + * + * @param query the query string + * @param parameters the query parameters as a map of name-value pairs (nullable) + * @param accessMode the access mode for the query (nullable) + * @return the serialized query as a byte array + */ public static byte[] serialize( String query, Map parameters, AccessMode accessMode) { try { diff --git a/tools/java/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java similarity index 71% rename from tools/java/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java rename to tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java index 99454637..b40b24c6 100644 --- a/tools/java/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java @@ -17,8 +17,21 @@ import org.alibaba.neug.driver.Results; import org.alibaba.neug.driver.internal.InternalResultSet; +/** + * Utility class for parsing database server responses. + * + *

This class converts Protocol Buffers response bytes into ResultSet objects that can be used to + * access query results. + */ public class ResponseParser { + /** + * Parses a response byte array into a ResultSet. + * + * @param response the response bytes from the database server + * @return a ResultSet containing the query results + * @throws RuntimeException if the response cannot be parsed + */ public static ResultSet parse(byte[] response) { try { Results.QueryResponse queryResponse = Results.QueryResponse.parseFrom(response); diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/AccessModeTest.java b/tools/java_driver/src/test/java/org/alibaba/neug/driver/AccessModeTest.java new file mode 100644 index 00000000..551eff5d --- /dev/null +++ b/tools/java_driver/src/test/java/org/alibaba/neug/driver/AccessModeTest.java @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import org.alibaba.neug.driver.utils.AccessMode; +import org.junit.jupiter.api.Test; + +/** Test class for {@link AccessMode}. */ +public class AccessModeTest { + + @Test + public void testAccessModeValues() { + assertEquals(4, AccessMode.values().length); + + assertNotNull(AccessMode.READ); + assertNotNull(AccessMode.INSERT); + assertNotNull(AccessMode.UPDATE); + assertNotNull(AccessMode.SCHEMA); + } + + @Test + public void testAccessModeValueOf() { + assertEquals(AccessMode.READ, AccessMode.valueOf("READ")); + assertEquals(AccessMode.INSERT, AccessMode.valueOf("INSERT")); + assertEquals(AccessMode.UPDATE, AccessMode.valueOf("UPDATE")); + assertEquals(AccessMode.SCHEMA, AccessMode.valueOf("SCHEMA")); + } + + @Test + public void testAccessModeName() { + assertEquals("READ", AccessMode.READ.name()); + assertEquals("INSERT", AccessMode.INSERT.name()); + assertEquals("UPDATE", AccessMode.UPDATE.name()); + assertEquals("SCHEMA", AccessMode.SCHEMA.name()); + } + + @Test + public void testInvalidAccessMode() { + assertThrows( + IllegalArgumentException.class, + () -> { + AccessMode.valueOf("INVALID"); + }); + } +} diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/ConfigTest.java b/tools/java_driver/src/test/java/org/alibaba/neug/driver/ConfigTest.java new file mode 100644 index 00000000..81ef9cdb --- /dev/null +++ b/tools/java_driver/src/test/java/org/alibaba/neug/driver/ConfigTest.java @@ -0,0 +1,74 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import org.alibaba.neug.driver.utils.Config; +import org.junit.jupiter.api.Test; + +/** Test class for {@link Config}. */ +public class ConfigTest { + + @Test + public void testDefaultConfig() { + Config config = Config.builder().build(); + + assertEquals(30000, config.getConnectionTimeoutMillis()); + assertEquals(30000, config.getReadTimeoutMillis()); + assertEquals(30000, config.getWriteTimeoutMillis()); + assertEquals(30000, config.getKeepAliveIntervalMillis()); + assertEquals(100, config.getMaxConnectionPoolSize()); + } + + @Test + public void testCustomConfig() { + Config config = + Config.builder() + .withConnectionTimeoutMillis(10000) + .withReadTimeoutMillis(20000) + .withWriteTimeoutMillis(15000) + .withKeepAliveIntervalMillis(60000) + .withMaxConnectionPoolSize(50) + .build(); + + assertEquals(10000, config.getConnectionTimeoutMillis()); + assertEquals(20000, config.getReadTimeoutMillis()); + assertEquals(15000, config.getWriteTimeoutMillis()); + assertEquals(60000, config.getKeepAliveIntervalMillis()); + assertEquals(50, config.getMaxConnectionPoolSize()); + } + + @Test + public void testBuilderChaining() { + Config.ConfigBuilder builder = Config.builder(); + Config config = + builder.withConnectionTimeoutMillis(5000) + .withReadTimeoutMillis(5000) + .withWriteTimeoutMillis(5000) + .build(); + + assertNotNull(config); + assertEquals(5000, config.getConnectionTimeoutMillis()); + } + + @Test + public void testConfigImmutability() { + Config config1 = Config.builder().withConnectionTimeoutMillis(10000).build(); + Config config2 = Config.builder().withConnectionTimeoutMillis(20000).build(); + + assertEquals(10000, config1.getConnectionTimeoutMillis()); + assertEquals(20000, config2.getConnectionTimeoutMillis()); + } +} diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/GraphDatabaseTest.java b/tools/java_driver/src/test/java/org/alibaba/neug/driver/GraphDatabaseTest.java new file mode 100644 index 00000000..70ad8f40 --- /dev/null +++ b/tools/java_driver/src/test/java/org/alibaba/neug/driver/GraphDatabaseTest.java @@ -0,0 +1,79 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import org.alibaba.neug.driver.utils.Config; +import org.junit.jupiter.api.Test; + +/** Test class for {@link GraphDatabase}. */ +public class GraphDatabaseTest { + + @Test + public void testDriverCreationWithUri() { + String uri = "http://localhost:8000"; + Driver driver = GraphDatabase.driver(uri); + + assertNotNull(driver); + assertFalse(driver.isClosed()); + + driver.close(); + assertTrue(driver.isClosed()); + } + + @Test + public void testDriverCreationWithUriAndConfig() { + String uri = "http://localhost:8000"; + Config config = Config.builder().withConnectionTimeoutMillis(5000).build(); + + Driver driver = GraphDatabase.driver(uri, config); + + assertNotNull(driver); + assertFalse(driver.isClosed()); + + driver.close(); + } + + @Test + public void testDriverCreationWithNullUri() { + assertThrows( + IllegalArgumentException.class, + () -> { + GraphDatabase.driver(null); + }); + } + + @Test + public void testDriverCreationWithEmptyUri() { + assertThrows( + IllegalArgumentException.class, + () -> { + GraphDatabase.driver(""); + }); + } + + @Test + public void testMultipleDriverInstances() { + Driver driver1 = GraphDatabase.driver("http://localhost:8000"); + Driver driver2 = GraphDatabase.driver("http://localhost:9000"); + + assertNotNull(driver1); + assertNotNull(driver2); + assertNotSame(driver1, driver2); + + driver1.close(); + driver2.close(); + } +} diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java b/tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java new file mode 100644 index 00000000..ff189229 --- /dev/null +++ b/tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java @@ -0,0 +1,402 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import com.google.protobuf.ByteString; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import org.alibaba.neug.driver.internal.InternalResultSet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Test class for {@link InternalResultSet}. */ +public class InternalResultSetTest { + + private Results.QueryResponse createSampleResponse() { + // Create a simple response with 3 rows and 2 columns (name: String, age: Int) + Results.StringArray nameArray = + Results.StringArray.newBuilder() + .addAllValues(Arrays.asList("Alice", "Bob", "Charlie")) + .build(); + + Results.Int32Array ageArray = + Results.Int32Array.newBuilder().addAllValues(Arrays.asList(30, 25, 35)).build(); + + Results.Array nameColumn = Results.Array.newBuilder().setStringArray(nameArray).build(); + Results.Array ageColumn = Results.Array.newBuilder().setInt32Array(ageArray).build(); + + Results.MetaDatas metaDatas = + Results.MetaDatas.newBuilder().addName("name").addName("age").build(); + + return Results.QueryResponse.newBuilder() + .addArrays(nameColumn) + .addArrays(ageColumn) + .setSchema(metaDatas) + .setRowCount(3) + .build(); + } + + private InternalResultSet resultSet; + + @BeforeEach + public void setUp() { + Results.QueryResponse response = createSampleResponse(); + resultSet = new InternalResultSet(response); + } + + @Test + public void testNext() { + assertTrue(resultSet.next()); + assertEquals(0, resultSet.getRow()); + + assertTrue(resultSet.next()); + assertEquals(1, resultSet.getRow()); + + assertTrue(resultSet.next()); + assertEquals(2, resultSet.getRow()); + + assertFalse(resultSet.next()); + } + + @Test + public void testPrevious() { + resultSet.absolute(2); + assertEquals(2, resultSet.getRow()); + + assertTrue(resultSet.previous()); + assertEquals(1, resultSet.getRow()); + + assertTrue(resultSet.previous()); + assertEquals(0, resultSet.getRow()); + + assertFalse(resultSet.previous()); + } + + @Test + public void testAbsolute() { + assertTrue(resultSet.absolute(0)); + assertEquals(0, resultSet.getRow()); + + assertTrue(resultSet.absolute(1)); + assertEquals(1, resultSet.getRow()); + + assertTrue(resultSet.absolute(2)); + assertEquals(2, resultSet.getRow()); + + assertFalse(resultSet.absolute(3)); + assertFalse(resultSet.absolute(-1)); + } + + @Test + public void testRelative() { + resultSet.absolute(0); + + assertTrue(resultSet.relative(1)); + assertEquals(1, resultSet.getRow()); + + assertTrue(resultSet.relative(1)); + assertEquals(2, resultSet.getRow()); + + assertFalse(resultSet.relative(1)); + + assertTrue(resultSet.relative(-1)); + assertEquals(1, resultSet.getRow()); + } + + @Test + public void testGetString() { + resultSet.next(); + assertEquals("Alice", resultSet.getString("name")); + assertEquals("Alice", resultSet.getString(0)); + } + + @Test + public void testGetInt() { + resultSet.next(); + assertEquals(30, resultSet.getInt("age")); + assertEquals(30, resultSet.getInt(1)); + } + + @Test + public void testGetColumnNames() { + List columnNames = resultSet.getColumnNames(); + assertEquals(2, columnNames.size()); + assertEquals("name", columnNames.get(0)); + assertEquals("age", columnNames.get(1)); + } + + @Test + public void testClose() { + assertFalse(resultSet.isClosed()); + resultSet.close(); + assertTrue(resultSet.isClosed()); + } + + @Test + public void testGetObjectAfterClose() { + resultSet.next(); + resultSet.close(); + assertTrue(resultSet.isClosed()); + + // Accessing after close should throw or return null + // This depends on implementation + } + + @Test + public void testGetInvalidColumn() { + resultSet.next(); + assertThrows( + RuntimeException.class, + () -> { + resultSet.getString("nonexistent"); + }); + } + + @Test + public void testGetInvalidColumnIndex() { + resultSet.next(); + assertThrows( + Exception.class, + () -> { + resultSet.getString(99); + }); + } + + @Test + public void testWasNull() { + // Create a response with NULL values + // Row 0: name="Alice", age=30 (no nulls) + // Row 1: name=NULL, age=25 + // Row 2: name="Charlie", age=NULL + Results.StringArray nameArray = + Results.StringArray.newBuilder() + .addAllValues(Arrays.asList("Alice", "", "Charlie")) + .setValidity( + ByteString.copyFrom(new byte[] {0b00000101})) // bit 1 is 0 (NULL) + .build(); + + Results.Int32Array ageArray = + Results.Int32Array.newBuilder() + .addAllValues(Arrays.asList(30, 25, 0)) + .setValidity( + ByteString.copyFrom(new byte[] {0b00000011})) // bit 2 is 0 (NULL) + .build(); + + Results.Array nameColumn = Results.Array.newBuilder().setStringArray(nameArray).build(); + Results.Array ageColumn = Results.Array.newBuilder().setInt32Array(ageArray).build(); + + Results.MetaDatas metaDatas = + Results.MetaDatas.newBuilder().addName("name").addName("age").build(); + + Results.QueryResponse response = + Results.QueryResponse.newBuilder() + .addArrays(nameColumn) + .addArrays(ageColumn) + .setSchema(metaDatas) + .setRowCount(3) + .build(); + + InternalResultSet rs = new InternalResultSet(response); + + // Row 0: both values are not null + rs.next(); + assertEquals("Alice", rs.getString("name")); + assertFalse(rs.wasNull()); + assertEquals(30, rs.getInt("age")); + assertFalse(rs.wasNull()); + + // Row 1: name is NULL, age is not null + rs.next(); + rs.getString("name"); + assertTrue(rs.wasNull()); + assertEquals(25, rs.getInt("age")); + assertFalse(rs.wasNull()); + + // Row 2: name is not null, age is NULL + rs.next(); + assertEquals("Charlie", rs.getString("name")); + assertFalse(rs.wasNull()); + rs.getInt("age"); + assertTrue(rs.wasNull()); + } + + @Test + public void testGetBigDecimal() { + // Create a response with various numeric types + Results.Int32Array int32Array = + Results.Int32Array.newBuilder().addAllValues(Arrays.asList(100, 200, 300)).build(); + + Results.Int64Array int64Array = + Results.Int64Array.newBuilder() + .addAllValues(Arrays.asList(1000L, 2000L, 3000L)) + .build(); + + Results.DoubleArray doubleArray = + Results.DoubleArray.newBuilder() + .addAllValues(Arrays.asList(10.5, 20.5, 30.5)) + .build(); + + Results.FloatArray floatArray = + Results.FloatArray.newBuilder() + .addAllValues(Arrays.asList(1.5f, 2.5f, 3.5f)) + .build(); + + Results.Array int32Column = Results.Array.newBuilder().setInt32Array(int32Array).build(); + Results.Array int64Column = Results.Array.newBuilder().setInt64Array(int64Array).build(); + Results.Array doubleColumn = Results.Array.newBuilder().setDoubleArray(doubleArray).build(); + Results.Array floatColumn = Results.Array.newBuilder().setFloatArray(floatArray).build(); + + Results.MetaDatas metaDatas = + Results.MetaDatas.newBuilder() + .addName("int32_col") + .addName("int64_col") + .addName("double_col") + .addName("float_col") + .build(); + + Results.QueryResponse response = + Results.QueryResponse.newBuilder() + .addArrays(int32Column) + .addArrays(int64Column) + .addArrays(doubleColumn) + .addArrays(floatColumn) + .setSchema(metaDatas) + .setRowCount(3) + .build(); + + InternalResultSet rs = new InternalResultSet(response); + + // Test first row + rs.next(); + assertEquals(new BigDecimal(100), rs.getBigDecimal("int32_col")); + assertEquals(new BigDecimal(1000L), rs.getBigDecimal("int64_col")); + assertEquals(BigDecimal.valueOf(10.5), rs.getBigDecimal("double_col")); + assertEquals(BigDecimal.valueOf(1.5f), rs.getBigDecimal("float_col")); + + // Test by column index + assertEquals(new BigDecimal(100), rs.getBigDecimal(0)); + assertEquals(new BigDecimal(1000L), rs.getBigDecimal(1)); + assertEquals(BigDecimal.valueOf(10.5), rs.getBigDecimal(2)); + assertEquals(BigDecimal.valueOf(1.5f), rs.getBigDecimal(3)); + } + + @Test + public void testGetBigDecimalWithNull() { + // Create a response with NULL values + Results.Int32Array int32Array = + Results.Int32Array.newBuilder() + .addAllValues(Arrays.asList(100, 0, 300)) + .setValidity( + ByteString.copyFrom(new byte[] {0b00000101})) // bit 1 is 0 (NULL) + .build(); + + Results.Array int32Column = Results.Array.newBuilder().setInt32Array(int32Array).build(); + + Results.MetaDatas metaDatas = Results.MetaDatas.newBuilder().addName("value").build(); + + Results.QueryResponse response = + Results.QueryResponse.newBuilder() + .addArrays(int32Column) + .setSchema(metaDatas) + .setRowCount(3) + .build(); + + InternalResultSet rs = new InternalResultSet(response); + + // Row 0: not null + rs.next(); + assertEquals(new BigDecimal(100), rs.getBigDecimal("value")); + assertFalse(rs.wasNull()); + + // Row 1: NULL + rs.next(); + assertEquals(BigDecimal.ZERO, rs.getBigDecimal("value")); + assertTrue(rs.wasNull()); + + // Row 2: not null + rs.next(); + assertEquals(new BigDecimal(300), rs.getBigDecimal("value")); + assertFalse(rs.wasNull()); + } + + @Test + public void testUnsignedIntegerOverflow() { + // Test uint32 overflow: value 3000000000 > Integer.MAX_VALUE (2147483647) + // In Java signed int, this would be -1294967296 (negative) + // We need to cast the long to int to represent the unsigned value + Results.UInt32Array uint32Array = + Results.UInt32Array.newBuilder() + .addValues((int) 3000000000L) // Value > Integer.MAX_VALUE, stored as + // negative int + .addValues(2147483647) // Integer.MAX_VALUE + .addValues(100) + .build(); + + // Test uint64 overflow: value > Long.MAX_VALUE (9223372036854775807) + // Value: 18446744073709551615 (max uint64) would be -1 as signed long + Results.UInt64Array uint64Array = + Results.UInt64Array.newBuilder() + .addValues(-1L) // This represents 18446744073709551615 as unsigned + .addValues(9223372036854775807L) // Long.MAX_VALUE + .addValues(1000L) + .build(); + + Results.Array uint32Column = Results.Array.newBuilder().setUint32Array(uint32Array).build(); + Results.Array uint64Column = Results.Array.newBuilder().setUint64Array(uint64Array).build(); + + Results.MetaDatas metaDatas = + Results.MetaDatas.newBuilder().addName("uint32_col").addName("uint64_col").build(); + + Results.QueryResponse response = + Results.QueryResponse.newBuilder() + .addArrays(uint32Column) + .addArrays(uint64Column) + .setSchema(metaDatas) + .setRowCount(3) + .build(); + + InternalResultSet rs = new InternalResultSet(response); + + // Row 0: Test uint32 overflow (3000000000) + rs.next(); + long uint32Value = rs.getLong("uint32_col"); + assertEquals(3000000000L, uint32Value, "uint32 value should be 3000000000 (not negative)"); + + // Test getBigDecimal for uint32 overflow + BigDecimal uint32Decimal = rs.getBigDecimal("uint32_col"); + assertEquals(new BigDecimal(3000000000L), uint32Decimal); + + // Test uint64 overflow (max uint64 = 18446744073709551615) + BigDecimal uint64Decimal = rs.getBigDecimal("uint64_col"); + assertEquals( + new BigDecimal("18446744073709551615"), + uint64Decimal, + "uint64 max value should be 18446744073709551615 (not negative)"); + + // Row 1: Test boundary values (MAX_VALUE for signed types) + rs.next(); + assertEquals(2147483647L, rs.getLong("uint32_col")); + assertEquals(new BigDecimal(2147483647L), rs.getBigDecimal("uint32_col")); + assertEquals(9223372036854775807L, rs.getLong("uint64_col")); + assertEquals(new BigDecimal(9223372036854775807L), rs.getBigDecimal("uint64_col")); + + // Row 2: Test normal values + rs.next(); + assertEquals(100L, rs.getLong("uint32_col")); + assertEquals(1000L, rs.getLong("uint64_col")); + } +} diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/JsonUtilTest.java b/tools/java_driver/src/test/java/org/alibaba/neug/driver/JsonUtilTest.java new file mode 100644 index 00000000..f53d1dd4 --- /dev/null +++ b/tools/java_driver/src/test/java/org/alibaba/neug/driver/JsonUtilTest.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.alibaba.neug.driver.utils.JsonUtil; +import org.junit.jupiter.api.Test; + +/** Test class for {@link JsonUtil}. */ +public class JsonUtilTest { + + @Test + public void testGetInstance() { + ObjectMapper mapper = JsonUtil.getInstance(); + assertNotNull(mapper); + } + + @Test + public void testSingletonPattern() { + ObjectMapper mapper1 = JsonUtil.getInstance(); + ObjectMapper mapper2 = JsonUtil.getInstance(); + + assertSame(mapper1, mapper2, "Should return the same instance"); + } + + @Test + public void testObjectMapperFunctionality() throws Exception { + ObjectMapper mapper = JsonUtil.getInstance(); + + String json = "{\"name\":\"test\",\"value\":123}"; + Object obj = mapper.readValue(json, Object.class); + + assertNotNull(obj); + } +} diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/QuerySerializerTest.java b/tools/java_driver/src/test/java/org/alibaba/neug/driver/QuerySerializerTest.java new file mode 100644 index 00000000..5e25ef2a --- /dev/null +++ b/tools/java_driver/src/test/java/org/alibaba/neug/driver/QuerySerializerTest.java @@ -0,0 +1,130 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.HashMap; +import java.util.Map; +import org.alibaba.neug.driver.utils.AccessMode; +import org.alibaba.neug.driver.utils.QuerySerializer; +import org.junit.jupiter.api.Test; + +/** Test class for {@link QuerySerializer}. */ +public class QuerySerializerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void testSerializeSimpleQuery() throws Exception { + String query = "MATCH (n) RETURN n"; + byte[] result = QuerySerializer.serialize(query); + + assertNotNull(result); + assertTrue(result.length > 0); + + JsonNode json = mapper.readTree(result); + assertEquals(query, json.get("query").asText()); + assertNull(json.get("parameters")); + assertNull(json.get("access_mode")); + } + + @Test + public void testSerializeQueryWithParameters() throws Exception { + String query = "MATCH (n:Person {name: $name}) RETURN n"; + Map parameters = new HashMap<>(); + parameters.put("name", "Alice"); + parameters.put("age", 30); + + byte[] result = QuerySerializer.serialize(query, parameters); + + assertNotNull(result); + JsonNode json = mapper.readTree(result); + assertEquals(query, json.get("query").asText()); + assertEquals("Alice", json.get("parameters").get("name").asText()); + assertEquals(30, json.get("parameters").get("age").asInt()); + } + + @Test + public void testSerializeQueryWithAccessMode() throws Exception { + String query = "MATCH (n) RETURN n"; + byte[] result = QuerySerializer.serialize(query, AccessMode.READ); + + assertNotNull(result); + JsonNode json = mapper.readTree(result); + assertEquals(query, json.get("query").asText()); + assertEquals("READ", json.get("access_mode").asText()); + } + + @Test + public void testSerializeQueryWithParametersAndAccessMode() throws Exception { + String query = "CREATE (n:Person {name: $name}) RETURN n"; + Map parameters = new HashMap<>(); + parameters.put("name", "Bob"); + + byte[] result = QuerySerializer.serialize(query, parameters, AccessMode.INSERT); + + assertNotNull(result); + JsonNode json = mapper.readTree(result); + assertEquals(query, json.get("query").asText()); + assertEquals("Bob", json.get("parameters").get("name").asText()); + assertEquals("INSERT", json.get("access_mode").asText()); + } + + @Test + public void testSerializeQueryWithComplexParameters() throws Exception { + String query = "CREATE (n:Person $props) RETURN n"; + Map parameters = new HashMap<>(); + Map props = new HashMap<>(); + props.put("name", "Charlie"); + props.put("age", 25); + props.put("active", true); + parameters.put("props", props); + + byte[] result = QuerySerializer.serialize(query, parameters); + + assertNotNull(result); + JsonNode json = mapper.readTree(result); + JsonNode propsNode = json.get("parameters").get("props"); + assertEquals("Charlie", propsNode.get("name").asText()); + assertEquals(25, propsNode.get("age").asInt()); + assertTrue(propsNode.get("active").asBoolean()); + } + + @Test + public void testSerializeWithNullParameters() throws Exception { + String query = "MATCH (n) RETURN n"; + Map nullParams = null; + byte[] result = QuerySerializer.serialize(query, nullParams); + + assertNotNull(result); + JsonNode json = mapper.readTree(result); + assertEquals(query, json.get("query").asText()); + assertNull(json.get("parameters")); + } + + @Test + public void testSerializeWithEmptyParameters() throws Exception { + String query = "MATCH (n) RETURN n"; + Map parameters = new HashMap<>(); + byte[] result = QuerySerializer.serialize(query, parameters); + + assertNotNull(result); + JsonNode json = mapper.readTree(result); + assertEquals(query, json.get("query").asText()); + assertTrue(json.get("parameters").isEmpty()); + } +} From 85ea06b16ba2f3f48be8c023fa3dfd7cd5416dc7 Mon Sep 17 00:00:00 2001 From: liulx20 Date: Thu, 12 Mar 2026 16:49:45 +0800 Subject: [PATCH 03/60] Update tools/java_driver/USAGE.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- tools/java_driver/USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/java_driver/USAGE.md b/tools/java_driver/USAGE.md index 636dddf1..49013f6a 100644 --- a/tools/java_driver/USAGE.md +++ b/tools/java_driver/USAGE.md @@ -24,7 +24,7 @@ mvn clean install -DskipTests ### Basic Connection ```java -import org.alibaba.neug.driver.*; +cd tools/java_driver public class Example { public static void main(String[] args) { From 3d8c283fc6408517ce4ee8d0209b8c36fc98eeeb Mon Sep 17 00:00:00 2001 From: liulx20 Date: Thu, 12 Mar 2026 16:50:02 +0800 Subject: [PATCH 04/60] Update tools/java_driver/USAGE.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- tools/java_driver/USAGE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/java_driver/USAGE.md b/tools/java_driver/USAGE.md index 49013f6a..3b1728f7 100644 --- a/tools/java_driver/USAGE.md +++ b/tools/java_driver/USAGE.md @@ -79,8 +79,9 @@ public class ConfigExample { } } } -``` - + Config config = Config.builder() + .withConnectionTimeoutMillis(3000) + .build(); ### Parameterized Query ```java From 91cf3846f7424136ba5e1ee84b42e3f790771876 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 12 Mar 2026 17:18:38 +0800 Subject: [PATCH 05/60] fix some issues --- .../main/java/org/alibaba/neug/driver/GraphDatabase.java | 5 +++-- .../org/alibaba/neug/driver/internal/InternalDriver.java | 4 ++-- .../main/java/org/alibaba/neug/driver/utils/Client.java | 9 +++++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/GraphDatabase.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/GraphDatabase.java index 3217b945..4a3877cc 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/GraphDatabase.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/GraphDatabase.java @@ -73,8 +73,9 @@ public static Driver driver(String uri, Config config) { if (!uri.startsWith("http://") && !uri.startsWith("https://")) { throw new IllegalArgumentException("URI must start with http:// or https://"); } - + if (config == null) { + throw new IllegalArgumentException("Config cannot be null"); + } return new InternalDriver(uri, config); } } -; diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java index 9bc0007b..ad6a007a 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java @@ -27,7 +27,7 @@ */ public class InternalDriver implements Driver { - private static Client client = null; + private Client client = null; /** * Constructs a new InternalDriver with the specified URI and configuration. @@ -36,7 +36,7 @@ public class InternalDriver implements Driver { * @param config the configuration for the driver */ public InternalDriver(String uri, Config config) { - client = new Client(uri, config); + this.client = new Client(uri, config); } @Override diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Client.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Client.java index 3977a9ca..3c1ed129 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Client.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Client.java @@ -20,6 +20,7 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import okhttp3.ResponseBody; /** * HTTP client for communicating with the NeuG database server. @@ -30,7 +31,7 @@ public class Client { private final String uri; - private static OkHttpClient httpClient = null; + private OkHttpClient httpClient = null; private boolean closed = false; /** @@ -71,7 +72,11 @@ public byte[] syncPost(byte[] request) throws IOException { if (!response.isSuccessful()) { throw new IOException("Unexpected code " + response); } - return response.body().bytes(); + ResponseBody responseBody = response.body(); + if (responseBody == null) { + throw new IOException("Response body is null"); + } + return responseBody.bytes(); } } From b96bcc80a4840f34f36addff47ca4cb87e270bc2 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 12 Mar 2026 17:46:08 +0800 Subject: [PATCH 06/60] add ClientTest --- .../org/alibaba/neug/driver/ClientTest.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tools/java_driver/src/test/java/org/alibaba/neug/driver/ClientTest.java diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/ClientTest.java b/tools/java_driver/src/test/java/org/alibaba/neug/driver/ClientTest.java new file mode 100644 index 00000000..f2347f00 --- /dev/null +++ b/tools/java_driver/src/test/java/org/alibaba/neug/driver/ClientTest.java @@ -0,0 +1,90 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import org.alibaba.neug.driver.utils.Client; +import org.alibaba.neug.driver.utils.Config; +import org.junit.jupiter.api.Test; + +/** Test class for {@link Client}. */ +public class ClientTest { + + @Test + public void testClientConstruction() { + Config config = Config.builder().build(); + Client client = new Client("http://localhost:8080", config); + assertNotNull(client); + assertFalse(client.isClosed()); + } + + @Test + public void testClientClose() { + Config config = Config.builder().build(); + Client client = new Client("http://localhost:8080", config); + assertFalse(client.isClosed()); + client.close(); + assertTrue(client.isClosed()); + } + + @Test + public void testClientWithCustomConfig() { + Config config = + Config.builder() + .withMaxConnectionPoolSize(20) + .withConnectionTimeoutMillis(5000) + .withReadTimeoutMillis(30000) + .withWriteTimeoutMillis(30000) + .withKeepAliveIntervalMillis(300000) + .build(); + Client client = new Client("http://localhost:8080", config); + assertNotNull(client); + assertFalse(client.isClosed()); + } + + @Test + public void testSyncPostThrowsExceptionWhenServerUnreachable() { + Config config = + Config.builder() + .withConnectionTimeoutMillis(1000) // Short timeout for faster test + .build(); + Client client = new Client("http://localhost:19999", config); // Non-existent server + + byte[] request = "test query".getBytes(); + assertThrows(IOException.class, () -> client.syncPost(request)); + } + + @Test + public void testMultipleClientsIndependence() { + Config config1 = Config.builder().build(); + Config config2 = Config.builder().build(); + + Client client1 = new Client("http://localhost:8080", config1); + Client client2 = new Client("http://localhost:9090", config2); + + assertNotNull(client1); + assertNotNull(client2); + assertFalse(client1.isClosed()); + assertFalse(client2.isClosed()); + + client1.close(); + assertTrue(client1.isClosed()); + assertFalse(client2.isClosed()); + + client2.close(); + assertTrue(client2.isClosed()); + } +} From c9887619f7298724d686ff6dfd21915db8095da5 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 12 Mar 2026 17:52:36 +0800 Subject: [PATCH 07/60] update doc --- tools/java_driver/USAGE.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tools/java_driver/USAGE.md b/tools/java_driver/USAGE.md index 3b1728f7..d77657d2 100644 --- a/tools/java_driver/USAGE.md +++ b/tools/java_driver/USAGE.md @@ -5,7 +5,7 @@ 1. Install to local Maven repository: ```bash -cd tools/java +cd tools/java_driver mvn clean install -DskipTests ``` @@ -24,7 +24,6 @@ mvn clean install -DskipTests ### Basic Connection ```java -cd tools/java_driver public class Example { public static void main(String[] args) { @@ -60,7 +59,7 @@ import org.alibaba.neug.driver.utils.*; public class ConfigExample { public static void main(String[] args) { Config config = Config.builder() - .withConnectionTimeout(3000) + .withConnectionTimeoutMillis(3000) .build(); Driver driver = GraphDatabase.driver("http://localhost:10000", config); @@ -79,9 +78,6 @@ public class ConfigExample { } } } - Config config = Config.builder() - .withConnectionTimeoutMillis(3000) - .build(); ### Parameterized Query ```java From b49ae7f5a6260c2f1b6e178d6afff33ae4fe52fc Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 12 Mar 2026 19:22:09 +0800 Subject: [PATCH 08/60] fix doc --- tools/java_driver/USAGE.md | 4 +- tools/java_driver/pom.xml | 4 +- .../org/alibaba/neug/driver/ResultSet.java | 20 +++++----- .../java/org/alibaba/neug/driver/Session.java | 8 ++-- .../neug/driver/internal/InternalDriver.java | 6 ++- .../driver/internal/InternalResultSet.java | 39 +++++++++---------- 6 files changed, 42 insertions(+), 39 deletions(-) diff --git a/tools/java_driver/USAGE.md b/tools/java_driver/USAGE.md index d77657d2..628034bc 100644 --- a/tools/java_driver/USAGE.md +++ b/tools/java_driver/USAGE.md @@ -64,7 +64,7 @@ public class ConfigExample { Driver driver = GraphDatabase.driver("http://localhost:10000", config); - try (Session session = driver.session(AccessMode.READ)) { + try (Session session = driver.session()) { // Read-only query try (ResultSet rs = session.run("MATCH (n:Person) RETURN n.name, n.age")) { while (rs.next()) { @@ -78,6 +78,8 @@ public class ConfigExample { } } } +``` + ### Parameterized Query ```java diff --git a/tools/java_driver/pom.xml b/tools/java_driver/pom.xml index 861a846b..9ec8fecf 100644 --- a/tools/java_driver/pom.xml +++ b/tools/java_driver/pom.xml @@ -15,8 +15,8 @@ UTF-8 - 8 - 8 + 9 + 9 5.9.3 2.0.7 4.29.6 diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java index a920d756..ace8d867 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java @@ -148,7 +148,7 @@ public interface ResultSet extends AutoCloseable { * to string. * * @param columnName the name of the column - * @return the column value; {@code null} if the value is SQL NULL + * @return the column value; * @throws IllegalArgumentException if the column name is not valid * @throws ClassCastException if the column is not of a compatible type */ @@ -161,7 +161,7 @@ public interface ResultSet extends AutoCloseable { * to string. * * @param columnIndex the column index (0-based) - * @return the column value; {@code null} if the value is SQL NULL + * @return the column value; * @throws IndexOutOfBoundsException if the column index is out of bounds * @throws ClassCastException if the column is not of a compatible type */ @@ -173,7 +173,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type DATE. * * @param columnName the name of the column - * @return the column value; {@code null} if the value is SQL NULL + * @return the column value; * @throws IllegalArgumentException if the column name is not valid * @throws ClassCastException if the column is not of type DATE */ @@ -185,7 +185,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type DATE. * * @param columnIndex the column index (0-based) - * @return the column value; {@code null} if the value is SQL NULL + * @return the column value; * @throws IndexOutOfBoundsException if the column index is out of bounds * @throws ClassCastException if the column is not of type DATE */ @@ -197,7 +197,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type TIMESTAMP. * * @param columnName the name of the column - * @return the column value; {@code null} if the value is SQL NULL + * @return the column value; * @throws IllegalArgumentException if the column name is not valid * @throws ClassCastException if the column is not of type TIMESTAMP */ @@ -209,7 +209,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type TIMESTAMP. * * @param columnIndex the column index (0-based) - * @return the column value; {@code null} if the value is SQL NULL + * @return the column value; * @throws IndexOutOfBoundsException if the column index is out of bounds * @throws ClassCastException if the column is not of type TIMESTAMP */ @@ -221,7 +221,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type BOOLEAN. * * @param columnName the name of the column - * @return the column value; {@code false} if the value is SQL NULL + * @return the column value; * @throws IllegalArgumentException if the column name is not valid * @throws ClassCastException if the column is not of type BOOLEAN */ @@ -233,7 +233,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type BOOLEAN. * * @param columnIndex the column index (0-based) - * @return the column value; {@code false} if the value is SQL NULL + * @return the column value; * @throws IndexOutOfBoundsException if the column index is out of bounds * @throws ClassCastException if the column is not of type BOOLEAN */ @@ -296,7 +296,7 @@ public interface ResultSet extends AutoCloseable { * precision is critical. * * @param columnName the name of the column - * @return the column value; {@code null} if the value is SQL NULL + * @return the column value; * @throws IllegalArgumentException if the column name is not valid * @throws ClassCastException if the column is not a numeric type */ @@ -311,7 +311,7 @@ public interface ResultSet extends AutoCloseable { * precision is critical. * * @param columnIndex the column index (0-based) - * @return the column value; {@code null} if the value is SQL NULL + * @return the column value; * @throws IndexOutOfBoundsException if the column index is out of bounds * @throws ClassCastException if the column is not a numeric type */ diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/Session.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/Session.java index de6ff418..7c52f199 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/Session.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/Session.java @@ -48,17 +48,17 @@ public interface Session extends AutoCloseable { * Executes a Cypher statement with configuration options. * * @param statement the Cypher query to execute - * @param config configuration options for query execution + * @param parameters query parameters as key-value pairs * @return a {@link ResultSet} containing the query results * @throws RuntimeException if the query fails */ - ResultSet run(String statement, Map config); + ResultSet run(String statement, Map parameters); /** * Executes a Cypher statement with a specific access mode. * * @param statement the Cypher query to execute - * @param mode the access mode (READ or WRITE) + * @param mode the access mode (READ/INSERT/UPDATE/SCHEMA) * @return a {@link ResultSet} containing the query results * @throws RuntimeException if the query fails */ @@ -69,7 +69,7 @@ public interface Session extends AutoCloseable { * * @param statement the Cypher query to execute * @param parameters query parameters as key-value pairs - * @param mode the access mode (READ or WRITE) + * @param mode the access mode (READ/INSERT/UPDATE/SCHEMA) * @return a {@link ResultSet} containing the query results * @throws RuntimeException if the query fails */ diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java index ad6a007a..eea96791 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java @@ -14,6 +14,7 @@ package org.alibaba.neug.driver.internal; import org.alibaba.neug.driver.Driver; +import org.alibaba.neug.driver.ResultSet; import org.alibaba.neug.driver.Session; import org.alibaba.neug.driver.utils.AccessMode; import org.alibaba.neug.driver.utils.Client; @@ -46,8 +47,9 @@ public Session session() { @Override public void verifyConnectivity() { - try (Session session = session()) { - session.run("RETURN 1", null, AccessMode.READ); + try (Session session = session(); + ResultSet rs = session.run("RETURN 1", null, AccessMode.READ)) { + // Execute query to verify connectivity, result is discarded } } diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java index 7fd9e781..baa137d7 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java @@ -105,12 +105,12 @@ public Object getObject(int columnIndex) { } } - private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted) + private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyHandled) throws Exception { switch (array.getTypedArrayCase()) { case STRING_ARRAY: { - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = array.getStringArray().getValidity(); was_null = !nullBitmap.isEmpty() @@ -121,7 +121,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted } case INT32_ARRAY: { - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = array.getInt32Array().getValidity(); was_null = !nullBitmap.isEmpty() @@ -132,7 +132,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted } case INT64_ARRAY: { - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = array.getInt64Array().getValidity(); was_null = !nullBitmap.isEmpty() @@ -143,7 +143,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted } case BOOL_ARRAY: { - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = array.getBoolArray().getValidity(); was_null = !nullBitmap.isEmpty() @@ -154,7 +154,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted } case DOUBLE_ARRAY: { - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = array.getDoubleArray().getValidity(); was_null = !nullBitmap.isEmpty() @@ -165,7 +165,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted } case TIMESTAMP_ARRAY: { - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = array.getTimestampArray().getValidity(); was_null = !nullBitmap.isEmpty() @@ -176,7 +176,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted } case DATE_ARRAY: { - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = array.getDateArray().getValidity(); was_null = !nullBitmap.isEmpty() @@ -189,7 +189,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { Results.ListArray listArray = array.getListArray(); - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = listArray.getValidity(); was_null = !nullBitmap.isEmpty() @@ -209,7 +209,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { Results.StructArray structArray = array.getStructArray(); - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = structArray.getValidity(); was_null = !nullBitmap.isEmpty() @@ -226,7 +226,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { Results.VertexArray vertexArray = array.getVertexArray(); ObjectMapper mapper = JsonUtil.getInstance(); - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = vertexArray.getValidity(); was_null = !nullBitmap.isEmpty() @@ -243,7 +243,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { Results.EdgeArray edgeArray = array.getEdgeArray(); ObjectMapper mapper = JsonUtil.getInstance(); - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = edgeArray.getValidity(); was_null = !nullBitmap.isEmpty() @@ -260,7 +260,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted { Results.PathArray pathArray = array.getPathArray(); ObjectMapper mapper = JsonUtil.getInstance(); - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = pathArray.getValidity(); was_null = !nullBitmap.isEmpty() @@ -276,8 +276,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted case INTERVAL_ARRAY: { Results.IntervalArray intervalArray = array.getIntervalArray(); - ObjectMapper mapper = JsonUtil.getInstance(); - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = intervalArray.getValidity(); was_null = !nullBitmap.isEmpty() @@ -289,7 +288,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted case UINT32_ARRAY: { Results.UInt32Array uint32Array = array.getUint32Array(); - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = uint32Array.getValidity(); was_null = !nullBitmap.isEmpty() @@ -302,7 +301,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean isNullSetted case UINT64_ARRAY: { Results.UInt64Array uint64Array = array.getUint64Array(); - if (!isNullSetted) { + if (!nullAlreadyHandled) { ByteString nullBitmap = uint64Array.getValidity(); was_null = !nullBitmap.isEmpty() @@ -546,10 +545,10 @@ public boolean isClosed() { } private int getColumnIndex(String columnName) { - Results.MetaDatas colum_name = response.getSchema(); - int columnCount = colum_name.getNameCount(); + Results.MetaDatas metaDatas = response.getSchema(); + int columnCount = metaDatas.getNameCount(); for (int i = 0; i < columnCount; i++) { - if (colum_name.getName(i).equals(columnName)) { + if (metaDatas.getName(i).equals(columnName)) { return i; } } From 64a2806880d7eae657ea3d824afa917a8b42dbc2 Mon Sep 17 00:00:00 2001 From: liulx20 Date: Thu, 12 Mar 2026 19:33:28 +0800 Subject: [PATCH 09/60] Update tools/java_driver/pom.xml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tools/java_driver/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/java_driver/pom.xml b/tools/java_driver/pom.xml index 9ec8fecf..d8b28466 100644 --- a/tools/java_driver/pom.xml +++ b/tools/java_driver/pom.xml @@ -80,8 +80,8 @@ maven-compiler-plugin 3.11.0 - 8 - 8 + ${maven.compiler.source} + ${maven.compiler.target} UTF-8 From 92c7e0de47a48d099239eef125f91c68c0d2bd7d Mon Sep 17 00:00:00 2001 From: liulx20 Date: Thu, 12 Mar 2026 19:34:24 +0800 Subject: [PATCH 10/60] Update tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../org/alibaba/neug/driver/InternalResultSetTest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java b/tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java index ff189229..88ba20d4 100644 --- a/tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java +++ b/tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java @@ -152,8 +152,12 @@ public void testGetObjectAfterClose() { resultSet.close(); assertTrue(resultSet.isClosed()); - // Accessing after close should throw or return null - // This depends on implementation + // Accessing after close is expected to fail + assertThrows( + Exception.class, + () -> { + resultSet.getString("name"); + }); } @Test From dddfe7113e900c7804f71b4a1264e97a80aad2d1 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 12 Mar 2026 19:54:51 +0800 Subject: [PATCH 11/60] format --- .../org/alibaba/neug/driver/ResultSet.java | 68 ++++++++++++++++--- .../driver/internal/InternalResultSet.java | 62 ++++++++++++++--- 2 files changed, 113 insertions(+), 17 deletions(-) diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java index ace8d867..872b59e3 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java @@ -148,7 +148,7 @@ public interface ResultSet extends AutoCloseable { * to string. * * @param columnName the name of the column - * @return the column value; + * @return the column value; * @throws IllegalArgumentException if the column name is not valid * @throws ClassCastException if the column is not of a compatible type */ @@ -161,7 +161,7 @@ public interface ResultSet extends AutoCloseable { * to string. * * @param columnIndex the column index (0-based) - * @return the column value; + * @return the column value; * @throws IndexOutOfBoundsException if the column index is out of bounds * @throws ClassCastException if the column is not of a compatible type */ @@ -173,7 +173,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type DATE. * * @param columnName the name of the column - * @return the column value; + * @return the column value; * @throws IllegalArgumentException if the column name is not valid * @throws ClassCastException if the column is not of type DATE */ @@ -185,7 +185,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type DATE. * * @param columnIndex the column index (0-based) - * @return the column value; + * @return the column value; * @throws IndexOutOfBoundsException if the column index is out of bounds * @throws ClassCastException if the column is not of type DATE */ @@ -197,7 +197,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type TIMESTAMP. * * @param columnName the name of the column - * @return the column value; + * @return the column value; * @throws IllegalArgumentException if the column name is not valid * @throws ClassCastException if the column is not of type TIMESTAMP */ @@ -209,7 +209,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type TIMESTAMP. * * @param columnIndex the column index (0-based) - * @return the column value; + * @return the column value; * @throws IndexOutOfBoundsException if the column index is out of bounds * @throws ClassCastException if the column is not of type TIMESTAMP */ @@ -221,7 +221,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type BOOLEAN. * * @param columnName the name of the column - * @return the column value; + * @return the column value; * @throws IllegalArgumentException if the column name is not valid * @throws ClassCastException if the column is not of type BOOLEAN */ @@ -233,7 +233,7 @@ public interface ResultSet extends AutoCloseable { *

Type requirement: The column must be of type BOOLEAN. * * @param columnIndex the column index (0-based) - * @return the column value; + * @return the column value; * @throws IndexOutOfBoundsException if the column index is out of bounds * @throws ClassCastException if the column is not of type BOOLEAN */ @@ -296,7 +296,7 @@ public interface ResultSet extends AutoCloseable { * precision is critical. * * @param columnName the name of the column - * @return the column value; + * @return the column value; * @throws IllegalArgumentException if the column name is not valid * @throws ClassCastException if the column is not a numeric type */ @@ -341,4 +341,54 @@ public interface ResultSet extends AutoCloseable { * @return a list of column names */ List getColumnNames(); + + /** Moves the cursor to the end of this ResultSet object, just after the last row. */ + void afterLast(); + + /** Moves the cursor to the beginning of this ResultSet object, just before the first row. */ + void beforeFirst(); + + /** + * Moves the cursor to the last row in this ResultSet object. + * + * @return {@code true} if the cursor is on a valid row; {@code false} if there are no rows in + * the result set + */ + boolean last(); + + /** + * Moves the cursor to the first row in this ResultSet object. + * + * @return {@code true} if the cursor is on a valid row; {@code false} if there are no rows in + * the result set + */ + boolean first(); + + /** + * Retrieves whether the cursor is on the last row of this ResultSet object. + * + * @return {@code true} if the cursor is on the last row; {@code false} otherwise + */ + boolean isLast(); + + /** + * Retrieves whether the cursor is on the first row of this ResultSet object. + * + * @return {@code true} if the cursor is on the first row; {@code false} otherwise + */ + boolean isFirst(); + + /** + * Retrieves whether the cursor is before the first row of this ResultSet object. + * + * @return {@code true} if the cursor is before the first row; {@code false} otherwise + */ + boolean isBeforeFirst(); + + /** + * Retrieves whether the cursor is after the last row of this ResultSet object. + * + * @return {@code true} if the cursor is after the last row; {@code false} otherwise + */ + boolean isAfterLast(); } diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java index baa137d7..23694a7f 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java @@ -328,13 +328,14 @@ public int getInt(String columnName) { public int getInt(int columnIndex) { checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); - if(arr.hasInt32Array()) { + if (arr.hasInt32Array()) { Results.Int32Array array = arr.getInt32Array(); ByteString nullBitmap = array.getValidity(); int value = array.getValues(currentIndex); was_null = !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) + == 0; return value; } return getNumericValue(arr).intValue(); @@ -350,13 +351,14 @@ public long getLong(String columnName) { public long getLong(int columnIndex) { checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); - if(arr.hasInt64Array()) { + if (arr.hasInt64Array()) { Results.Int64Array array = arr.getInt64Array(); ByteString nullBitmap = array.getValidity(); long value = array.getValues(currentIndex); was_null = !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) + == 0; return value; } return getNumericValue(arr).longValue(); @@ -460,13 +462,14 @@ public double getDouble(String columnName) { public double getDouble(int columnIndex) { checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); - if(arr.hasFloatArray()) { + if (arr.hasFloatArray()) { Results.FloatArray array = arr.getFloatArray(); ByteString nullBitmap = array.getValidity(); float value = array.getValues(currentIndex); was_null = !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) + == 0; return value; } return getNumericValue(arr).doubleValue(); @@ -482,13 +485,14 @@ public float getFloat(String columnName) { public float getFloat(int columnIndex) { checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); - if(arr.hasFloatArray()) { + if (arr.hasFloatArray()) { Results.FloatArray array = arr.getFloatArray(); ByteString nullBitmap = array.getValidity(); float value = array.getValues(currentIndex); was_null = !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) + == 0; return value; } return getNumericValue(arr).floatValue(); @@ -636,6 +640,48 @@ private Number getNumericValue(Results.Array arr) { throw new ClassCastException("Column is not a numeric type"); } + @Override + void afterLast() { + currentIndex = response.getArraysCount(); + } + + @Override + void beforeFirst() { + currentIndex = -1; + } + + @Override + boolean first() { + currentIndex = 0; + return currentIndex < response.getArraysCount(); + } + + @Override + boolean last() { + currentIndex = response.getArraysCount() - 1; + return currentIndex >= 0; + } + + @Override + boolean isFirst() { + return currentIndex == 0 && response.getArraysCount() != 0; + } + + @Override + boolean isLast() { + return currentIndex == response.getArraysCount() - 1 && response.getArraysCount() != 0; + } + + @Override + boolean isBeforeFirst() { + return currentIndex == -1 && response.getArraysCount() != 0; + } + + @Override + boolean isAfterLast() { + return currentIndex == response.getArraysCount() && response.getArraysCount() != 0; + } + private Results.QueryResponse response; private int currentIndex; private boolean was_null; From f495c20574b2e4db6302cfd8ac7d63d0fc616187 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 12 Mar 2026 20:14:26 +0800 Subject: [PATCH 12/60] rename org to com --- proto/response.proto | 2 +- .../alibaba/neug/driver/Driver.java | 2 +- .../alibaba/neug/driver/GraphDatabase.java | 6 ++--- .../alibaba/neug/driver/ResultSet.java | 2 +- .../alibaba/neug/driver/Session.java | 4 ++-- .../neug/driver/internal/InternalDriver.java | 14 +++++------ .../driver/internal/InternalResultSet.java | 24 +++++++++---------- .../neug/driver/internal/InternalSession.java | 14 +++++------ .../alibaba/neug/driver/utils/AccessMode.java | 2 +- .../alibaba/neug/driver/utils/Client.java | 2 +- .../alibaba/neug/driver/utils/Config.java | 2 +- .../alibaba/neug/driver/utils/JsonUtil.java | 2 +- .../neug/driver/utils/QuerySerializer.java | 2 +- .../neug/driver/utils/ResponseParser.java | 8 +++---- .../alibaba/neug/driver/AccessModeTest.java | 4 ++-- .../alibaba/neug/driver/ClientTest.java | 6 ++--- .../alibaba/neug/driver/ConfigTest.java | 4 ++-- .../neug/driver/GraphDatabaseTest.java | 4 ++-- .../neug/driver/InternalResultSetTest.java | 17 ++----------- .../alibaba/neug/driver/JsonUtilTest.java | 4 ++-- .../neug/driver/QuerySerializerTest.java | 6 ++--- 21 files changed, 59 insertions(+), 72 deletions(-) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/Driver.java (98%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/GraphDatabase.java (95%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/ResultSet.java (99%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/Session.java (97%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/internal/InternalDriver.java (85%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/internal/InternalResultSet.java (98%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/internal/InternalSession.java (87%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/utils/AccessMode.java (97%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/utils/Client.java (98%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/utils/Config.java (99%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/utils/JsonUtil.java (97%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/utils/QuerySerializer.java (98%) rename tools/java_driver/src/main/java/{org => com}/alibaba/neug/driver/utils/ResponseParser.java (88%) rename tools/java_driver/src/test/java/{org => com}/alibaba/neug/driver/AccessModeTest.java (95%) rename tools/java_driver/src/test/java/{org => com}/alibaba/neug/driver/ClientTest.java (96%) rename tools/java_driver/src/test/java/{org => com}/alibaba/neug/driver/ConfigTest.java (97%) rename tools/java_driver/src/test/java/{org => com}/alibaba/neug/driver/GraphDatabaseTest.java (96%) rename tools/java_driver/src/test/java/{org => com}/alibaba/neug/driver/InternalResultSetTest.java (96%) rename tools/java_driver/src/test/java/{org => com}/alibaba/neug/driver/JsonUtilTest.java (94%) rename tools/java_driver/src/test/java/{org => com}/alibaba/neug/driver/QuerySerializerTest.java (97%) diff --git a/proto/response.proto b/proto/response.proto index 08e2d8ac..4f3a1e4f 100644 --- a/proto/response.proto +++ b/proto/response.proto @@ -15,7 +15,7 @@ */ syntax="proto3"; package neug; -option java_package = "org.alibaba.neug.driver"; +option java_package = "com.alibaba.neug.driver"; option java_outer_classname = "Results"; option cc_generic_services = true; diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/Driver.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/Driver.java similarity index 98% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/Driver.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/Driver.java index 89fc7a3a..64406916 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/Driver.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/Driver.java @@ -11,7 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; import java.io.Closeable; diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/GraphDatabase.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/GraphDatabase.java similarity index 95% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/GraphDatabase.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/GraphDatabase.java index 4a3877cc..948146be 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/GraphDatabase.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/GraphDatabase.java @@ -11,10 +11,10 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; -import org.alibaba.neug.driver.internal.InternalDriver; -import org.alibaba.neug.driver.utils.Config; +import com.alibaba.neug.driver.internal.InternalDriver; +import com.alibaba.neug.driver.utils.Config; /** * Main entry point for creating NeuG database driver connections. diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java similarity index 99% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java index 872b59e3..2f788bfe 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/ResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java @@ -11,7 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; import java.math.BigDecimal; import java.sql.Date; diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/Session.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/Session.java similarity index 97% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/Session.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/Session.java index 7c52f199..a6823320 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/Session.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/Session.java @@ -11,10 +11,10 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; import java.util.Map; -import org.alibaba.neug.driver.utils.AccessMode; +import com.alibaba.neug.driver.utils.AccessMode; /** * A session for executing queries against a NeuG database. diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalDriver.java similarity index 85% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalDriver.java index eea96791..b354411f 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalDriver.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalDriver.java @@ -11,14 +11,14 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver.internal; +package com.alibaba.neug.driver.internal; -import org.alibaba.neug.driver.Driver; -import org.alibaba.neug.driver.ResultSet; -import org.alibaba.neug.driver.Session; -import org.alibaba.neug.driver.utils.AccessMode; -import org.alibaba.neug.driver.utils.Client; -import org.alibaba.neug.driver.utils.Config; +import com.alibaba.neug.driver.Driver; +import com.alibaba.neug.driver.ResultSet; +import com.alibaba.neug.driver.Session; +import com.alibaba.neug.driver.utils.AccessMode; +import com.alibaba.neug.driver.utils.Client; +import com.alibaba.neug.driver.utils.Config; /** * Internal implementation of the {@link Driver} interface. diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java similarity index 98% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index 23694a7f..5921fcdd 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -11,7 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver.internal; +package com.alibaba.neug.driver.internal; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -23,9 +23,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import org.alibaba.neug.driver.ResultSet; -import org.alibaba.neug.driver.Results; -import org.alibaba.neug.driver.utils.JsonUtil; +import com.alibaba.neug.driver.ResultSet; +import com.alibaba.neug.driver.Results; +import com.alibaba.neug.driver.utils.JsonUtil; /** * Internal implementation of the {@link ResultSet} interface. @@ -641,44 +641,44 @@ private Number getNumericValue(Results.Array arr) { } @Override - void afterLast() { + public void afterLast() { currentIndex = response.getArraysCount(); } @Override - void beforeFirst() { + public void beforeFirst() { currentIndex = -1; } @Override - boolean first() { + public boolean first() { currentIndex = 0; return currentIndex < response.getArraysCount(); } @Override - boolean last() { + public boolean last() { currentIndex = response.getArraysCount() - 1; return currentIndex >= 0; } @Override - boolean isFirst() { + public boolean isFirst() { return currentIndex == 0 && response.getArraysCount() != 0; } @Override - boolean isLast() { + public boolean isLast() { return currentIndex == response.getArraysCount() - 1 && response.getArraysCount() != 0; } @Override - boolean isBeforeFirst() { + public boolean isBeforeFirst() { return currentIndex == -1 && response.getArraysCount() != 0; } @Override - boolean isAfterLast() { + public boolean isAfterLast() { return currentIndex == response.getArraysCount() && response.getArraysCount() != 0; } diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java similarity index 87% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java index 5f8f72e8..a527e58b 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/internal/InternalSession.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java @@ -11,15 +11,15 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver.internal; +package com.alibaba.neug.driver.internal; import java.util.Map; -import org.alibaba.neug.driver.ResultSet; -import org.alibaba.neug.driver.Session; -import org.alibaba.neug.driver.utils.AccessMode; -import org.alibaba.neug.driver.utils.Client; -import org.alibaba.neug.driver.utils.QuerySerializer; -import org.alibaba.neug.driver.utils.ResponseParser; +import com.alibaba.neug.driver.ResultSet; +import com.alibaba.neug.driver.Session; +import com.alibaba.neug.driver.utils.AccessMode; +import com.alibaba.neug.driver.utils.Client; +import com.alibaba.neug.driver.utils.QuerySerializer; +import com.alibaba.neug.driver.utils.ResponseParser; /** * Internal implementation of the {@link Session} interface. diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/AccessMode.java similarity index 97% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/AccessMode.java index 9f52356b..b1cc44a5 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/AccessMode.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/AccessMode.java @@ -11,7 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver.utils; +package com.alibaba.neug.driver.utils; /** * Enumeration of access modes for database operations. diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Client.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java similarity index 98% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Client.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java index 3c1ed129..1a3195f3 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Client.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java @@ -11,7 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver.utils; +package com.alibaba.neug.driver.utils; import java.io.IOException; import java.util.concurrent.TimeUnit; diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Config.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Config.java similarity index 99% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Config.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Config.java index 15b05956..b51f40e3 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/Config.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Config.java @@ -11,7 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver.utils; +package com.alibaba.neug.driver.utils; import java.io.Serializable; diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/JsonUtil.java similarity index 97% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/JsonUtil.java index 8781f91a..ff445c56 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/JsonUtil.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/JsonUtil.java @@ -11,7 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver.utils; +package com.alibaba.neug.driver.utils; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/QuerySerializer.java similarity index 98% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/QuerySerializer.java index 76bce478..95d23f1b 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/QuerySerializer.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/QuerySerializer.java @@ -11,7 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver.utils; +package com.alibaba.neug.driver.utils; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; diff --git a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/ResponseParser.java similarity index 88% rename from tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java rename to tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/ResponseParser.java index b40b24c6..9fc3663d 100644 --- a/tools/java_driver/src/main/java/org/alibaba/neug/driver/utils/ResponseParser.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/ResponseParser.java @@ -11,11 +11,11 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver.utils; +package com.alibaba.neug.driver.utils; -import org.alibaba.neug.driver.ResultSet; -import org.alibaba.neug.driver.Results; -import org.alibaba.neug.driver.internal.InternalResultSet; +import com.alibaba.neug.driver.ResultSet; +import com.alibaba.neug.driver.Results; +import com.alibaba.neug.driver.internal.InternalResultSet; /** * Utility class for parsing database server responses. diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/AccessModeTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/AccessModeTest.java similarity index 95% rename from tools/java_driver/src/test/java/org/alibaba/neug/driver/AccessModeTest.java rename to tools/java_driver/src/test/java/com/alibaba/neug/driver/AccessModeTest.java index 551eff5d..9149ef23 100644 --- a/tools/java_driver/src/test/java/org/alibaba/neug/driver/AccessModeTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/AccessModeTest.java @@ -11,11 +11,11 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; import static org.junit.jupiter.api.Assertions.*; -import org.alibaba.neug.driver.utils.AccessMode; +import com.alibaba.neug.driver.utils.AccessMode; import org.junit.jupiter.api.Test; /** Test class for {@link AccessMode}. */ diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/ClientTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/ClientTest.java similarity index 96% rename from tools/java_driver/src/test/java/org/alibaba/neug/driver/ClientTest.java rename to tools/java_driver/src/test/java/com/alibaba/neug/driver/ClientTest.java index f2347f00..fbe8c720 100644 --- a/tools/java_driver/src/test/java/org/alibaba/neug/driver/ClientTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/ClientTest.java @@ -11,13 +11,13 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; import static org.junit.jupiter.api.Assertions.*; import java.io.IOException; -import org.alibaba.neug.driver.utils.Client; -import org.alibaba.neug.driver.utils.Config; +import com.alibaba.neug.driver.utils.Client; +import com.alibaba.neug.driver.utils.Config; import org.junit.jupiter.api.Test; /** Test class for {@link Client}. */ diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/ConfigTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/ConfigTest.java similarity index 97% rename from tools/java_driver/src/test/java/org/alibaba/neug/driver/ConfigTest.java rename to tools/java_driver/src/test/java/com/alibaba/neug/driver/ConfigTest.java index 81ef9cdb..4b4e3b0a 100644 --- a/tools/java_driver/src/test/java/org/alibaba/neug/driver/ConfigTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/ConfigTest.java @@ -11,11 +11,11 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; import static org.junit.jupiter.api.Assertions.*; -import org.alibaba.neug.driver.utils.Config; +import com.alibaba.neug.driver.utils.Config; import org.junit.jupiter.api.Test; /** Test class for {@link Config}. */ diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/GraphDatabaseTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/GraphDatabaseTest.java similarity index 96% rename from tools/java_driver/src/test/java/org/alibaba/neug/driver/GraphDatabaseTest.java rename to tools/java_driver/src/test/java/com/alibaba/neug/driver/GraphDatabaseTest.java index 70ad8f40..35f25bf2 100644 --- a/tools/java_driver/src/test/java/org/alibaba/neug/driver/GraphDatabaseTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/GraphDatabaseTest.java @@ -11,11 +11,11 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; import static org.junit.jupiter.api.Assertions.*; -import org.alibaba.neug.driver.utils.Config; +import com.alibaba.neug.driver.utils.Config; import org.junit.jupiter.api.Test; /** Test class for {@link GraphDatabase}. */ diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java similarity index 96% rename from tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java rename to tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java index 88ba20d4..bbffe483 100644 --- a/tools/java_driver/src/test/java/org/alibaba/neug/driver/InternalResultSetTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java @@ -11,7 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; import static org.junit.jupiter.api.Assertions.*; @@ -19,7 +19,7 @@ import java.math.BigDecimal; import java.util.Arrays; import java.util.List; -import org.alibaba.neug.driver.internal.InternalResultSet; +import com.alibaba.neug.driver.internal.InternalResultSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -146,19 +146,6 @@ public void testClose() { assertTrue(resultSet.isClosed()); } - @Test - public void testGetObjectAfterClose() { - resultSet.next(); - resultSet.close(); - assertTrue(resultSet.isClosed()); - - // Accessing after close is expected to fail - assertThrows( - Exception.class, - () -> { - resultSet.getString("name"); - }); - } @Test public void testGetInvalidColumn() { diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/JsonUtilTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JsonUtilTest.java similarity index 94% rename from tools/java_driver/src/test/java/org/alibaba/neug/driver/JsonUtilTest.java rename to tools/java_driver/src/test/java/com/alibaba/neug/driver/JsonUtilTest.java index f53d1dd4..4ebdb572 100644 --- a/tools/java_driver/src/test/java/org/alibaba/neug/driver/JsonUtilTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JsonUtilTest.java @@ -11,12 +11,12 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; import static org.junit.jupiter.api.Assertions.*; import com.fasterxml.jackson.databind.ObjectMapper; -import org.alibaba.neug.driver.utils.JsonUtil; +import com.alibaba.neug.driver.utils.JsonUtil; import org.junit.jupiter.api.Test; /** Test class for {@link JsonUtil}. */ diff --git a/tools/java_driver/src/test/java/org/alibaba/neug/driver/QuerySerializerTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/QuerySerializerTest.java similarity index 97% rename from tools/java_driver/src/test/java/org/alibaba/neug/driver/QuerySerializerTest.java rename to tools/java_driver/src/test/java/com/alibaba/neug/driver/QuerySerializerTest.java index 5e25ef2a..03dfcff0 100644 --- a/tools/java_driver/src/test/java/org/alibaba/neug/driver/QuerySerializerTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/QuerySerializerTest.java @@ -11,7 +11,7 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.alibaba.neug.driver; +package com.alibaba.neug.driver; import static org.junit.jupiter.api.Assertions.*; @@ -19,8 +19,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.HashMap; import java.util.Map; -import org.alibaba.neug.driver.utils.AccessMode; -import org.alibaba.neug.driver.utils.QuerySerializer; +import com.alibaba.neug.driver.utils.AccessMode; +import com.alibaba.neug.driver.utils.QuerySerializer; import org.junit.jupiter.api.Test; /** Test class for {@link QuerySerializer}. */ From 9dbdcf091ede95c0bc7779002498eefd94505430 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 12 Mar 2026 20:17:22 +0800 Subject: [PATCH 13/60] fix doc --- tools/java_driver/USAGE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/java_driver/USAGE.md b/tools/java_driver/USAGE.md index 628034bc..a5421bc7 100644 --- a/tools/java_driver/USAGE.md +++ b/tools/java_driver/USAGE.md @@ -12,7 +12,7 @@ mvn clean install -DskipTests 2. Add dependency to your project's `pom.xml`: ```xml - org.alibaba.neug + com.alibaba.neug neug-java-driver 1.0.0-SNAPSHOT @@ -53,8 +53,8 @@ public class Example { ### Connection with Configuration ```java -import org.alibaba.neug.driver.*; -import org.alibaba.neug.driver.utils.*; +import com.alibaba.neug.driver.*; +import com.alibaba.neug.driver.utils.*; public class ConfigExample { public static void main(String[] args) { From 0a03f31d33173192201de38a7c32cebbf15254a1 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 12 Mar 2026 21:55:34 +0800 Subject: [PATCH 14/60] add result metadata --- .../com/alibaba/neug/driver/ResultSet.java | 7 + .../neug/driver/ResultSetMetaData.java | 83 +++++++++++ .../java/com/alibaba/neug/driver/Session.java | 2 +- .../driver/internal/InternalResultSet.java | 129 +++++++++++++++- .../internal/InternalResultSetMetaData.java | 123 ++++++++++++++++ .../neug/driver/internal/InternalSession.java | 2 +- .../com/alibaba/neug/driver/utils/Types.java | 139 ++++++++++++++++++ .../com/alibaba/neug/driver/ClientTest.java | 2 +- .../neug/driver/InternalResultSetTest.java | 3 +- .../com/alibaba/neug/driver/JsonUtilTest.java | 2 +- .../neug/driver/QuerySerializerTest.java | 4 +- 11 files changed, 485 insertions(+), 11 deletions(-) create mode 100644 tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSetMetaData.java create mode 100644 tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSetMetaData.java create mode 100644 tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java index 2f788bfe..48c8179e 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java @@ -391,4 +391,11 @@ public interface ResultSet extends AutoCloseable { * @return {@code true} if the cursor is after the last row; {@code false} otherwise */ boolean isAfterLast(); + + /** + * Retrieves the number, types and properties of this ResultSet object's columns. + * + * @return the description of this ResultSet object's columns + */ + ResultSetMetaData getMetaData(); } diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSetMetaData.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSetMetaData.java new file mode 100644 index 00000000..376a2500 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSetMetaData.java @@ -0,0 +1,83 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.neug.driver; + +/** + * Provides information about the types and properties of the columns in a {@link ResultSet}. + * + *

Example usage: + * + *

{@code
+ * ResultSet results = session.run("MATCH (n:Person) RETURN n.name, n.age");
+ * ResultSetMetaData metaData = results.getMetaData();
+ * int columnCount = metaData.getColumnCount();
+ * for (int i = 0; i < columnCount; i++) {
+ *     System.out.println("Column " + i + ": " + metaData.getColumnName(i) +
+ *                        " (type: " + metaData.getColumnTypeName(i) + ")");
+ * }
+ * }
+ */ +public interface ResultSetMetaData { + /** + * Returns the number of columns in the result set. + * + * @return the number of columns + */ + int getColumnCount(); + + /** + * Gets the designated column's name. + * + * @param column the column index (0-based) + * @return the column name + * @throws IndexOutOfBoundsException if the column index is out of bounds + */ + String getColumnName(int column); + + /** + * Retrieves the designated column's SQL type code. + * + * @param column the column index (0-based) + * @return the SQL type code as defined in {@link java.sql.Types} + * @throws IndexOutOfBoundsException if the column index is out of bounds + */ + int getColumnType(int column); + + /** + * Retrieves the designated column's database-specific type name. + * + * @param column the column index (0-based) + * @return the type name used by the database + * @throws IndexOutOfBoundsException if the column index is out of bounds + */ + String getColumnTypeName(int column); + + /** + * Indicates the nullability of values in the designated column. + * + * @param column the column index (0-based) + * @return the nullability status (0 = no nulls, 1 = nullable, 2 = unknown) + * @throws IndexOutOfBoundsException if the column index is out of bounds + */ + int isNullable(int column); + + /** + * Indicates whether values in the designated column are signed numbers. + * + * @param column the column index (0-based) + * @return {@code true} if the column contains signed numeric values; {@code false} otherwise + * @throws IndexOutOfBoundsException if the column index is out of bounds + */ + boolean isSigned(int column); +} diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/Session.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/Session.java index a6823320..44370a7d 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/Session.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/Session.java @@ -13,8 +13,8 @@ */ package com.alibaba.neug.driver; -import java.util.Map; import com.alibaba.neug.driver.utils.AccessMode; +import java.util.Map; /** * A session for executing queries against a NeuG database. diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index 5921fcdd..30c9e674 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -13,6 +13,11 @@ */ package com.alibaba.neug.driver.internal; +import com.alibaba.neug.driver.ResultSet; +import com.alibaba.neug.driver.ResultSetMetaData; +import com.alibaba.neug.driver.Results; +import com.alibaba.neug.driver.utils.JsonUtil; +import com.alibaba.neug.driver.utils.Types; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.protobuf.ByteString; @@ -23,9 +28,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import com.alibaba.neug.driver.ResultSet; -import com.alibaba.neug.driver.Results; -import com.alibaba.neug.driver.utils.JsonUtil; /** * Internal implementation of the {@link ResultSet} interface. @@ -682,6 +684,127 @@ public boolean isAfterLast() { return currentIndex == response.getArraysCount() && response.getArraysCount() != 0; } + @Override + public ResultSetMetaData getMetaData() { + Results.MetaDatas metaDatas = response.getSchema(); + List columnNames = new ArrayList<>(); + List columnNullability = new ArrayList<>(); + List columnTypes = new ArrayList<>(); + List columnSigned = new ArrayList<>(); + for (int i = 0; i < metaDatas.getNameCount(); i++) { + columnNames.add(metaDatas.getName(i)); + switch (response.getArrays(i).getTypedArrayCase()) { + case STRING_ARRAY: + columnTypes.add(Types.STRING); + columnNullability.add( + response.getArrays(i).getStringArray().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(false); + break; + case INT32_ARRAY: + columnTypes.add(Types.INT32); + columnNullability.add( + response.getArrays(i).getInt32Array().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(true); + break; + case INT64_ARRAY: + columnTypes.add(Types.INT64); + columnNullability.add( + response.getArrays(i).getInt64Array().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(true); + break; + case BOOL_ARRAY: + columnTypes.add(Types.BOOLEAN); + columnNullability.add( + response.getArrays(i).getBoolArray().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(false); + break; + case DOUBLE_ARRAY: + columnTypes.add(Types.DOUBLE); + columnNullability.add( + response.getArrays(i).getDoubleArray().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(true); + break; + case FLOAT_ARRAY: + columnTypes.add(Types.FLOAT); + columnNullability.add( + response.getArrays(i).getFloatArray().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(true); + break; + case TIMESTAMP_ARRAY: + columnTypes.add(Types.TIMESTAMP); + columnNullability.add( + response.getArrays(i).getTimestampArray().getValidity().isEmpty() + ? 0 + : 1); + columnSigned.add(false); + break; + case DATE_ARRAY: + columnTypes.add(Types.DATE); + columnNullability.add( + response.getArrays(i).getDateArray().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(false); + break; + case UINT32_ARRAY: + columnTypes.add(Types.UINT32); + columnNullability.add( + response.getArrays(i).getUint32Array().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(false); + break; + case UINT64_ARRAY: + columnTypes.add(Types.UINT64); + columnNullability.add( + response.getArrays(i).getUint64Array().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(false); + break; + case LIST_ARRAY: + columnTypes.add(Types.LIST); + columnNullability.add( + response.getArrays(i).getListArray().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(false); + break; + case STRUCT_ARRAY: + columnTypes.add(Types.STRUCT); + columnNullability.add( + response.getArrays(i).getStructArray().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(false); + break; + case VERTEX_ARRAY: + columnTypes.add(Types.NODE); + columnNullability.add( + response.getArrays(i).getVertexArray().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(false); + break; + case EDGE_ARRAY: + columnTypes.add(Types.EDGE); + columnNullability.add( + response.getArrays(i).getEdgeArray().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(false); + break; + case PATH_ARRAY: + columnTypes.add(Types.PATH); + columnNullability.add( + response.getArrays(i).getPathArray().getValidity().isEmpty() ? 0 : 1); + columnSigned.add(false); + break; + case INTERVAL_ARRAY: + columnTypes.add(Types.INTERVAL); + columnNullability.add( + response.getArrays(i).getIntervalArray().getValidity().isEmpty() + ? 0 + : 1); + columnSigned.add(false); + break; + default: + // For complex types, we can set type as OTHER and nullability as unknown + columnTypes.add(Types.OTHER); + columnNullability.add(2); // unknown + columnSigned.add(false); + } + } + return new InternalResultSetMetaData( + columnNames, columnNullability, columnTypes, columnSigned); + } + private Results.QueryResponse response; private int currentIndex; private boolean was_null; diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSetMetaData.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSetMetaData.java new file mode 100644 index 00000000..79b1f339 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSetMetaData.java @@ -0,0 +1,123 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.neug.driver.internal; + +import com.alibaba.neug.driver.ResultSetMetaData; +import com.alibaba.neug.driver.utils.Types; +import java.util.List; + +/** + * Internal implementation of {@link ResultSetMetaData} that provides metadata information about the + * columns in a result set. + * + *

This class stores metadata for all columns including their names, types, nullability, and sign + * information. It provides methods to query this information for individual columns. + * + *

Column indices are 0-based internally but the interface methods use standard JDBC conventions. + * + *

Example usage: + * + *

{@code
+ * ResultSetMetaData metaData = resultSet.getMetaData();
+ * int columnCount = metaData.getColumnCount();
+ * for (int i = 0; i < columnCount; i++) {
+ *     String name = metaData.getColumnName(i);
+ *     String typeName = metaData.getColumnTypeName(i);
+ *     System.out.println(name + ": " + typeName);
+ * }
+ * }
+ * + * @see ResultSetMetaData + * @see Types + */ +public class InternalResultSetMetaData implements ResultSetMetaData { + /** + * Constructs a new InternalResultSetMetaData with the specified column metadata. + * + * @param columnNames the list of column names + * @param columnNullability the list of column nullability values (from {@link + * ResultSetMetaData}) + * @param columnTypes the list of column types + * @param columnSigned the list of boolean values indicating if columns are signed + */ + public InternalResultSetMetaData( + List columnNames, + List columnNullability, + List columnTypes, + List columnSigned) { + this.columnNames = columnNames; + this.columnNullability = columnNullability; + this.columnTypes = columnTypes; + this.columnSigned = columnSigned; + } + + /** + * Validates that the given column index is within valid bounds. + * + * @param column the column index to validate + * @throws IndexOutOfBoundsException if the column index is out of bounds + */ + private void validateColumnIndex(int column) { + if (column < 0 || column >= columnNames.size()) { + throw new IndexOutOfBoundsException("Invalid column index: " + column); + } + } + + @Override + public int getColumnCount() { + return columnNames.size(); + } + + @Override + public String getColumnName(int column) { + validateColumnIndex(column); + return columnNames.get(column); + } + + @Override + public int getColumnType(int column) { + validateColumnIndex(column); + return columnTypes.get(column).getJdbcType(); + } + + @Override + public boolean isSigned(int column) { + validateColumnIndex(column); + return columnSigned.get(column); + } + + @Override + public int isNullable(int column) { + validateColumnIndex(column); + return columnNullability.get(column); + } + + @Override + public String getColumnTypeName(int column) { + validateColumnIndex(column); + return columnTypes.get(column).name(); + } + + /** The list of column names in the result set. */ + private List columnNames; + + /** The list of column nullability values. */ + private List columnNullability; + + /** The list of column data types. */ + private List columnTypes; + + /** The list indicating whether each column contains signed numeric values. */ + private List columnSigned; +} diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java index a527e58b..d183ade6 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java @@ -13,13 +13,13 @@ */ package com.alibaba.neug.driver.internal; -import java.util.Map; import com.alibaba.neug.driver.ResultSet; import com.alibaba.neug.driver.Session; import com.alibaba.neug.driver.utils.AccessMode; import com.alibaba.neug.driver.utils.Client; import com.alibaba.neug.driver.utils.QuerySerializer; import com.alibaba.neug.driver.utils.ResponseParser; +import java.util.Map; /** * Internal implementation of the {@link Session} interface. diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java new file mode 100644 index 00000000..1119c0a7 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java @@ -0,0 +1,139 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.neug.driver.utils; + +/** + * Enumeration of data types supported by NeuG database. + * + *

This enum maps to the primitive types defined in the Protocol Buffer schema and provides + * corresponding JDBC SQL type codes for compatibility with standard database interfaces. + * + *

Example usage: + * + *

{@code
+ * Types type = Types.STRING;
+ * int jdbcType = type.getJdbcType(); // Returns java.sql.Types.VARCHAR
+ * String typeName = type.getTypeName(); // Returns "STRING"
+ * }
+ */ +public enum Types { + /** Any type - represents a value of unknown or dynamic type. */ + ANY("ANY", java.sql.Types.OTHER), + + /** 32-bit signed integer. */ + INT32("INT32", java.sql.Types.INTEGER), + + /** 32-bit unsigned integer. */ + UINT32("UINT32", java.sql.Types.INTEGER), + + /** 64-bit signed integer (long). */ + INT64("INT64", java.sql.Types.BIGINT), + + /** 64-bit unsigned integer (unsigned long). */ + UINT64("UINT64", java.sql.Types.BIGINT), + /** Boolean value (true/false). */ + BOOLEAN("BOOLEAN", java.sql.Types.BOOLEAN), + + /** 32-bit floating point number. */ + FLOAT("FLOAT", java.sql.Types.FLOAT), + + /** 64-bit floating point number (double precision). */ + DOUBLE("DOUBLE", java.sql.Types.DOUBLE), + + /** Variable-length character string. */ + STRING("STRING", java.sql.Types.VARCHAR), + /** Fixed-precision decimal number. */ + DECIMAL("DECIMAL", java.sql.Types.DECIMAL), + + /** Date value (year, month, day). */ + DATE("DATE", java.sql.Types.DATE), + + /** Time value (hour, minute, second). */ + TIME("TIME", java.sql.Types.TIME), + + /** Timestamp value (date and time). */ + TIMESTAMP("TIMESTAMP", java.sql.Types.TIMESTAMP), + /** Binary data (byte array). */ + BYTES("BYTES", java.sql.Types.VARBINARY), + + /** Null value - represents the absence of a value. */ + NULL("NULL", java.sql.Types.NULL), + + /** List/array of values. */ + LIST("LIST", java.sql.Types.ARRAY), + + /** Map/dictionary of key-value pairs. */ + MAP("MAP", java.sql.Types.JAVA_OBJECT), + /** Graph node/vertex. */ + NODE("NODE", java.sql.Types.JAVA_OBJECT), + + /** Graph edge/relationship. */ + EDGE("EDGE", java.sql.Types.JAVA_OBJECT), + + /** Graph path. */ + PATH("PATH", java.sql.Types.JAVA_OBJECT), + + /** Struct/record type. */ + STRUCT("STRUCT", java.sql.Types.STRUCT), + + /** Interval type - represents a time interval. */ + INTERVAL("INTERVAL", java.sql.Types.OTHER), + + /** Other/unknown type. */ + OTHER("OTHER", java.sql.Types.OTHER); + + private final String typeName; + private final int jdbcType; + + /** + * Constructs a Types enum value. + * + * @param typeName the human-readable name of the type + * @param jdbcType the corresponding JDBC SQL type code from {@link java.sql.Types} + */ + Types(String typeName, int jdbcType) { + this.typeName = typeName; + this.jdbcType = jdbcType; + } + + /** + * Returns the human-readable name of this type. + * + * @return the type name as a string + */ + public String getTypeName() { + return typeName; + } + + /** + * Returns the JDBC SQL type code for this type. + * + *

The returned value corresponds to constants defined in {@link java.sql.Types}. + * + * @return the JDBC type code + */ + public int getJdbcType() { + return jdbcType; + } + + /** + * Returns the type name as the string representation. + * + * @return the type name + */ + @Override + public String toString() { + return typeName; + } +} diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/ClientTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/ClientTest.java index fbe8c720..f94d3fb1 100644 --- a/tools/java_driver/src/test/java/com/alibaba/neug/driver/ClientTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/ClientTest.java @@ -15,9 +15,9 @@ import static org.junit.jupiter.api.Assertions.*; -import java.io.IOException; import com.alibaba.neug.driver.utils.Client; import com.alibaba.neug.driver.utils.Config; +import java.io.IOException; import org.junit.jupiter.api.Test; /** Test class for {@link Client}. */ diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java index bbffe483..e42ea63b 100644 --- a/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java @@ -15,11 +15,11 @@ import static org.junit.jupiter.api.Assertions.*; +import com.alibaba.neug.driver.internal.InternalResultSet; import com.google.protobuf.ByteString; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; -import com.alibaba.neug.driver.internal.InternalResultSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -146,7 +146,6 @@ public void testClose() { assertTrue(resultSet.isClosed()); } - @Test public void testGetInvalidColumn() { resultSet.next(); diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JsonUtilTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JsonUtilTest.java index 4ebdb572..7ac49876 100644 --- a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JsonUtilTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JsonUtilTest.java @@ -15,8 +15,8 @@ import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.databind.ObjectMapper; import com.alibaba.neug.driver.utils.JsonUtil; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; /** Test class for {@link JsonUtil}. */ diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/QuerySerializerTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/QuerySerializerTest.java index 03dfcff0..e9454d8a 100644 --- a/tools/java_driver/src/test/java/com/alibaba/neug/driver/QuerySerializerTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/QuerySerializerTest.java @@ -15,12 +15,12 @@ import static org.junit.jupiter.api.Assertions.*; +import com.alibaba.neug.driver.utils.AccessMode; +import com.alibaba.neug.driver.utils.QuerySerializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.HashMap; import java.util.Map; -import com.alibaba.neug.driver.utils.AccessMode; -import com.alibaba.neug.driver.utils.QuerySerializer; import org.junit.jupiter.api.Test; /** Test class for {@link QuerySerializer}. */ From ec3724b387e336763a57443ea368a8639c8c5eb3 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Fri, 13 Mar 2026 19:42:49 +0800 Subject: [PATCH 15/60] fix --- tools/java_driver/pom.xml | 2 +- .../neug/driver/internal/InternalResultSet.java | 15 ++++++++------- .../java/com/alibaba/neug/driver/utils/Types.java | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tools/java_driver/pom.xml b/tools/java_driver/pom.xml index d8b28466..b397f9ab 100644 --- a/tools/java_driver/pom.xml +++ b/tools/java_driver/pom.xml @@ -5,7 +5,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.alibaba.neug + com.alibaba.neug neug-java-driver 1.0.0-SNAPSHOT jar diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index 30c9e674..e902ec00 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -644,7 +644,8 @@ private Number getNumericValue(Results.Array arr) { @Override public void afterLast() { - currentIndex = response.getArraysCount(); + // Position the cursor just after the last row + currentIndex = response.getRowCount(); } @Override @@ -655,33 +656,33 @@ public void beforeFirst() { @Override public boolean first() { currentIndex = 0; - return currentIndex < response.getArraysCount(); + return currentIndex < response.getRowCount(); } @Override public boolean last() { - currentIndex = response.getArraysCount() - 1; + currentIndex = response.getRowCount() - 1; return currentIndex >= 0; } @Override public boolean isFirst() { - return currentIndex == 0 && response.getArraysCount() != 0; + return currentIndex == 0 && response.getRowCount() != 0; } @Override public boolean isLast() { - return currentIndex == response.getArraysCount() - 1 && response.getArraysCount() != 0; + return currentIndex == response.getRowCount() - 1 && response.getRowCount() != 0; } @Override public boolean isBeforeFirst() { - return currentIndex == -1 && response.getArraysCount() != 0; + return currentIndex == -1 && response.getRowCount() != 0; } @Override public boolean isAfterLast() { - return currentIndex == response.getArraysCount() && response.getArraysCount() != 0; + return currentIndex == response.getRowCount() && response.getRowCount() != 0; } @Override diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java index 1119c0a7..565ccb56 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java @@ -88,7 +88,7 @@ public enum Types { STRUCT("STRUCT", java.sql.Types.STRUCT), /** Interval type - represents a time interval. */ - INTERVAL("INTERVAL", java.sql.Types.OTHER), + INTERVAL("INTERVAL", java.sql.Types.VARCHAR), /** Other/unknown type. */ OTHER("OTHER", java.sql.Types.OTHER); From eab1c9013f00ee21192c0b567c99d314696b199d Mon Sep 17 00:00:00 2001 From: liulx20 Date: Mon, 16 Mar 2026 10:29:41 +0800 Subject: [PATCH 16/60] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/main/java/com/alibaba/neug/driver/utils/Client.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java index 1a3195f3..f90b36a7 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java @@ -41,7 +41,7 @@ public class Client { * @param config the configuration for connection pooling and timeouts */ public Client(String uri, Config config) { - this.uri = uri + "/cypher"; + this.uri = (uri != null && uri.endsWith("/")) ? uri + "cypher" : uri + "/cypher"; this.closed = false; httpClient = From 814071418503bc131ca3c977ebe8136b88955843 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Mon, 16 Mar 2026 11:00:18 +0800 Subject: [PATCH 17/60] add tests --- .../neug/driver/ResultSetMetaData.java | 8 +- .../internal/InternalResultSetMetaData.java | 4 +- .../com/alibaba/neug/driver/utils/Client.java | 8 ++ .../com/alibaba/neug/driver/utils/Types.java | 66 +++++------- .../alibaba/neug/driver/AccessModeTest.java | 2 - .../driver/InternalResultSetMetaDataTest.java | 100 ++++++++++++++++++ .../neug/driver/JavaDriverE2ETest.java | 59 +++++++++++ 7 files changed, 199 insertions(+), 48 deletions(-) create mode 100644 tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetMetaDataTest.java create mode 100644 tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSetMetaData.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSetMetaData.java index 376a2500..94371e7b 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSetMetaData.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSetMetaData.java @@ -13,6 +13,8 @@ */ package com.alibaba.neug.driver; +import com.alibaba.neug.driver.utils.Types; + /** * Provides information about the types and properties of the columns in a {@link ResultSet}. * @@ -46,13 +48,13 @@ public interface ResultSetMetaData { String getColumnName(int column); /** - * Retrieves the designated column's SQL type code. + * Retrieves the designated column's native NeuG type. * * @param column the column index (0-based) - * @return the SQL type code as defined in {@link java.sql.Types} + * @return the native NeuG type enum * @throws IndexOutOfBoundsException if the column index is out of bounds */ - int getColumnType(int column); + Types getColumnType(int column); /** * Retrieves the designated column's database-specific type name. diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSetMetaData.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSetMetaData.java index 79b1f339..f7b646dc 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSetMetaData.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSetMetaData.java @@ -86,9 +86,9 @@ public String getColumnName(int column) { } @Override - public int getColumnType(int column) { + public Types getColumnType(int column) { validateColumnIndex(column); - return columnTypes.get(column).getJdbcType(); + return columnTypes.get(column); } @Override diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java index 1a3195f3..ccfa48f7 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java @@ -98,6 +98,14 @@ public boolean isClosed() { public void close() { if (!closed) { httpClient.connectionPool().evictAll(); + httpClient.dispatcher().executorService().shutdown(); + if (httpClient.cache() != null) { + try { + httpClient.cache().close(); + } catch (IOException ignored) { + // Ignored: best-effort cache close. + } + } closed = true; } } diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java index 565ccb56..983723df 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.java @@ -16,95 +16,90 @@ /** * Enumeration of data types supported by NeuG database. * - *

This enum maps to the primitive types defined in the Protocol Buffer schema and provides - * corresponding JDBC SQL type codes for compatibility with standard database interfaces. + *

This enum represents the native value types exposed by the NeuG Java driver. * *

Example usage: * *

{@code
  * Types type = Types.STRING;
- * int jdbcType = type.getJdbcType(); // Returns java.sql.Types.VARCHAR
  * String typeName = type.getTypeName(); // Returns "STRING"
  * }
*/ public enum Types { /** Any type - represents a value of unknown or dynamic type. */ - ANY("ANY", java.sql.Types.OTHER), + ANY("ANY"), /** 32-bit signed integer. */ - INT32("INT32", java.sql.Types.INTEGER), + INT32("INT32"), /** 32-bit unsigned integer. */ - UINT32("UINT32", java.sql.Types.INTEGER), + UINT32("UINT32"), /** 64-bit signed integer (long). */ - INT64("INT64", java.sql.Types.BIGINT), + INT64("INT64"), /** 64-bit unsigned integer (unsigned long). */ - UINT64("UINT64", java.sql.Types.BIGINT), + UINT64("UINT64"), /** Boolean value (true/false). */ - BOOLEAN("BOOLEAN", java.sql.Types.BOOLEAN), + BOOLEAN("BOOLEAN"), /** 32-bit floating point number. */ - FLOAT("FLOAT", java.sql.Types.FLOAT), + FLOAT("FLOAT"), /** 64-bit floating point number (double precision). */ - DOUBLE("DOUBLE", java.sql.Types.DOUBLE), + DOUBLE("DOUBLE"), /** Variable-length character string. */ - STRING("STRING", java.sql.Types.VARCHAR), + STRING("STRING"), /** Fixed-precision decimal number. */ - DECIMAL("DECIMAL", java.sql.Types.DECIMAL), + DECIMAL("DECIMAL"), /** Date value (year, month, day). */ - DATE("DATE", java.sql.Types.DATE), + DATE("DATE"), /** Time value (hour, minute, second). */ - TIME("TIME", java.sql.Types.TIME), + TIME("TIME"), /** Timestamp value (date and time). */ - TIMESTAMP("TIMESTAMP", java.sql.Types.TIMESTAMP), + TIMESTAMP("TIMESTAMP"), /** Binary data (byte array). */ - BYTES("BYTES", java.sql.Types.VARBINARY), + BYTES("BYTES"), /** Null value - represents the absence of a value. */ - NULL("NULL", java.sql.Types.NULL), + NULL("NULL"), /** List/array of values. */ - LIST("LIST", java.sql.Types.ARRAY), + LIST("LIST"), /** Map/dictionary of key-value pairs. */ - MAP("MAP", java.sql.Types.JAVA_OBJECT), + MAP("MAP"), /** Graph node/vertex. */ - NODE("NODE", java.sql.Types.JAVA_OBJECT), + NODE("NODE"), /** Graph edge/relationship. */ - EDGE("EDGE", java.sql.Types.JAVA_OBJECT), + EDGE("EDGE"), /** Graph path. */ - PATH("PATH", java.sql.Types.JAVA_OBJECT), + PATH("PATH"), /** Struct/record type. */ - STRUCT("STRUCT", java.sql.Types.STRUCT), + STRUCT("STRUCT"), /** Interval type - represents a time interval. */ - INTERVAL("INTERVAL", java.sql.Types.VARCHAR), + INTERVAL("INTERVAL"), /** Other/unknown type. */ - OTHER("OTHER", java.sql.Types.OTHER); + OTHER("OTHER"); private final String typeName; - private final int jdbcType; /** * Constructs a Types enum value. * * @param typeName the human-readable name of the type - * @param jdbcType the corresponding JDBC SQL type code from {@link java.sql.Types} */ - Types(String typeName, int jdbcType) { + Types(String typeName) { this.typeName = typeName; - this.jdbcType = jdbcType; } /** @@ -116,17 +111,6 @@ public String getTypeName() { return typeName; } - /** - * Returns the JDBC SQL type code for this type. - * - *

The returned value corresponds to constants defined in {@link java.sql.Types}. - * - * @return the JDBC type code - */ - public int getJdbcType() { - return jdbcType; - } - /** * Returns the type name as the string representation. * diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/AccessModeTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/AccessModeTest.java index 9149ef23..22af3a02 100644 --- a/tools/java_driver/src/test/java/com/alibaba/neug/driver/AccessModeTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/AccessModeTest.java @@ -23,8 +23,6 @@ public class AccessModeTest { @Test public void testAccessModeValues() { - assertEquals(4, AccessMode.values().length); - assertNotNull(AccessMode.READ); assertNotNull(AccessMode.INSERT); assertNotNull(AccessMode.UPDATE); diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetMetaDataTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetMetaDataTest.java new file mode 100644 index 00000000..f489b09f --- /dev/null +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetMetaDataTest.java @@ -0,0 +1,100 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import com.alibaba.neug.driver.internal.InternalResultSet; +import com.alibaba.neug.driver.utils.Types; +import com.google.protobuf.ByteString; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +/** Test class for {@link ResultSetMetaData}. */ +public class InternalResultSetMetaDataTest { + + @Test + public void testMetaDataReturnsNativeTypesAndColumnProperties() { + Results.StringArray nameArray = + Results.StringArray.newBuilder() + .addAllValues(Arrays.asList("Alice", "", "Charlie")) + .setValidity(ByteString.copyFrom(new byte[] {0b00000101})) + .build(); + + Results.Int64Array scoreArray = + Results.Int64Array.newBuilder().addAllValues(Arrays.asList(10L, 20L, 30L)).build(); + + Results.BoolArray activeArray = + Results.BoolArray.newBuilder() + .addAllValues(Arrays.asList(true, false, true)) + .build(); + + Results.QueryResponse response = + Results.QueryResponse.newBuilder() + .addArrays(Results.Array.newBuilder().setStringArray(nameArray).build()) + .addArrays(Results.Array.newBuilder().setInt64Array(scoreArray).build()) + .addArrays(Results.Array.newBuilder().setBoolArray(activeArray).build()) + .setSchema( + Results.MetaDatas.newBuilder() + .addName("name") + .addName("score") + .addName("active") + .build()) + .setRowCount(3) + .build(); + + InternalResultSet resultSet = new InternalResultSet(response); + ResultSetMetaData metaData = resultSet.getMetaData(); + + assertEquals(3, metaData.getColumnCount()); + + assertEquals("name", metaData.getColumnName(0)); + assertEquals(Types.STRING, metaData.getColumnType(0)); + assertEquals("STRING", metaData.getColumnTypeName(0)); + assertEquals(1, metaData.isNullable(0)); + assertFalse(metaData.isSigned(0)); + + assertEquals("score", metaData.getColumnName(1)); + assertEquals(Types.INT64, metaData.getColumnType(1)); + assertEquals("INT64", metaData.getColumnTypeName(1)); + assertEquals(0, metaData.isNullable(1)); + assertTrue(metaData.isSigned(1)); + + assertEquals("active", metaData.getColumnName(2)); + assertEquals(Types.BOOLEAN, metaData.getColumnType(2)); + assertEquals("BOOLEAN", metaData.getColumnTypeName(2)); + assertEquals(0, metaData.isNullable(2)); + assertFalse(metaData.isSigned(2)); + } + + @Test + public void testMetaDataRejectsInvalidColumnIndex() { + Results.StringArray nameArray = + Results.StringArray.newBuilder().addAllValues(Arrays.asList("Alice")).build(); + + Results.QueryResponse response = + Results.QueryResponse.newBuilder() + .addArrays(Results.Array.newBuilder().setStringArray(nameArray).build()) + .setSchema(Results.MetaDatas.newBuilder().addName("name").build()) + .setRowCount(1) + .build(); + + ResultSetMetaData metaData = new InternalResultSet(response).getMetaData(); + + assertThrows(IndexOutOfBoundsException.class, () -> metaData.getColumnName(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> metaData.getColumnType(1)); + assertThrows(IndexOutOfBoundsException.class, () -> metaData.isNullable(2)); + assertThrows(IndexOutOfBoundsException.class, () -> metaData.isSigned(3)); + } +} diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java new file mode 100644 index 00000000..d6e463ba --- /dev/null +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java @@ -0,0 +1,59 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.alibaba.neug.driver.utils.Types; +import org.junit.jupiter.api.Test; + +/** + * End-to-end integration test for the Java driver. + * + *

This test is skipped unless `NEUG_JAVA_DRIVER_E2E_URI` is set, so it is safe to keep in the + * default test suite. Example: + * + *

{@code
+ * NEUG_JAVA_DRIVER_E2E_URI=http://localhost:10000 mvn -Dtest=JavaDriverE2ETest test
+ * }
+ */ +public class JavaDriverE2ETest { + + private static final String E2E_URI_ENV = "NEUG_JAVA_DRIVER_E2E_URI"; + + @Test + public void testDriverCanQueryLiveServer() { + String uri = System.getenv(E2E_URI_ENV); + assumeTrue(uri != null && !uri.isBlank(), E2E_URI_ENV + " is not set"); + + try (Driver driver = GraphDatabase.driver(uri)) { + assertFalse(driver.isClosed()); + driver.verifyConnectivity(); + + try (Session session = driver.session(); + ResultSet resultSet = session.run("RETURN 1 AS value")) { + assertTrue(resultSet.next()); + assertEquals(1, resultSet.getInt("value")); + assertEquals(1, resultSet.getObject(0)); + assertFalse(resultSet.wasNull()); + assertEquals(Types.INT32, resultSet.getMetaData().getColumnType(0)); + assertEquals("value", resultSet.getMetaData().getColumnName(0)); + assertFalse(resultSet.next()); + } + } + } +} From fed0f4894340cb007b05e7a70d2de0db5b3e38ab Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Mon, 16 Mar 2026 16:16:46 +0800 Subject: [PATCH 18/60] add doc --- .github/workflows/docs.yml | 12 ++ .github/workflows/format-check.yml | 13 ++ .github/workflows/neug-test.yml | 18 ++- bin/benchmark.cc | 2 +- doc/source/reference/_meta.ts | 1 + doc/source/reference/index.rst | 6 + doc/source/reference/java_api/index.md | 160 +++++++++++++++++++++++++ 7 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 doc/source/reference/java_api/index.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1c3d5b5a..ae29a182 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,6 +6,7 @@ on: - main paths: - 'doc/**' + - 'tools/java_driver/**' - '.github/workflows/docs.yml' @@ -32,11 +33,20 @@ jobs: with: python-version: '3.10' + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: 'maven' + - name: Build Documentation run: | sudo apt update sudo apt install -y doxygen graphviz + cd ${GITHUB_WORKSPACE}/tools/java_driver + mvn -DskipTests javadoc:javadoc cd ${GITHUB_WORKSPACE}/tools/python_bind python3 -m pip install -r requirements.txt python3 -m pip install -r requirements_dev.txt @@ -44,4 +54,6 @@ jobs: . /home/neug/.neug_env make dependencies make html + mkdir -p ${GITHUB_WORKSPACE}/doc/build/html/reference/java_api + cp -r ${GITHUB_WORKSPACE}/tools/java_driver/target/site/apidocs ${GITHUB_WORKSPACE}/doc/build/html/reference/java_api/ diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 8766dab1..85335108 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -84,6 +84,13 @@ jobs: with: python-version: '3.10' + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: 'maven' + - name: Get PR Changes uses: dorny/paths-filter@v3 id: changes @@ -151,4 +158,10 @@ jobs: (python3 -m flake8 .) || (print_error "flake8 check failed" "tests/e2e" "flake8" && exit 1) popd + - name: Java Format Check + run: | + pushd ${GITHUB_WORKSPACE}/tools/java_driver + mvn spotless:check + popd + diff --git a/.github/workflows/neug-test.yml b/.github/workflows/neug-test.yml index d57bc1fa..3c771f0b 100644 --- a/.github/workflows/neug-test.yml +++ b/.github/workflows/neug-test.yml @@ -13,6 +13,7 @@ on: - 'proto/**' - 'third_party/**' - 'tools/python_bind/**' + - 'tools/java_driver/**' - 'tests/**' - '.github/workflows/neug-test.yml' - 'scripts/install_deps.sh' @@ -28,6 +29,7 @@ on: - 'proto/**' - 'third_party/**' - 'tools/python_bind/**' + - 'tools/java_driver/**' - 'tests/**' - '.github/workflows/neug-test.yml' - 'scripts/install_deps.sh' @@ -70,6 +72,7 @@ jobs: - 'proto/**' - 'third_party/**' - 'tools/python_bind/**' + - 'tools/java_driver/**' - 'tests/**' - '!tests/extension/**' - '!tests/extensions/**' @@ -94,7 +97,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/ccache - key: ${{ runner.os }}-ccache-${{ github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'src/**', 'include/**', 'proto/**', 'tools/python_bind/**', '.github/workflows/neug-test.yml') }} + key: ${{ runner.os }}-ccache-${{ github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'src/**', 'include/**', 'proto/**', 'tools/python_bind/**', 'tools/java_driver/**', '.github/workflows/neug-test.yml') }} restore-keys: | ${{ runner.os }}-ccache-${{ github.ref_name }}- ${{ runner.os }}-ccache-main- @@ -132,6 +135,13 @@ jobs: with: python-version: '3.13' + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: 'maven' + - name: Set ENABLE_GCOV for main branch if: steps.scope.outputs.extension_only != 'true' && (github.ref == 'refs/heads/main' && github.repository == 'alibaba/neug' && github.event_name == 'push') run: | @@ -338,6 +348,12 @@ jobs: python3 -m pytest --cov=neug --cov-report=term --exitfirst --cov-append --cov-config=.coveragerc -sv tests/test_ngcli_commands.py python3 -m pytest --cov=neug --cov-report=term --exitfirst --cov-append --cov-config=.coveragerc -sv tests/test_ngcli_basics.py + - name: Run Java Driver Test + if: steps.scope.outputs.extension_only != 'true' + run: | + cd ${GITHUB_WORKSPACE}/tools/java_driver/ + mvn test + # ======================================== # Phase 4: E2E Tests # ======================================== diff --git a/bin/benchmark.cc b/bin/benchmark.cc index a1cd6e5e..8cf14b57 100644 --- a/bin/benchmark.cc +++ b/bin/benchmark.cc @@ -233,7 +233,7 @@ void benchmark_iteration( } int main(int argc, char** argv) { - cxxopts::Options options("rt_server", "Real-time graph server for NeuG"); + cxxopts::Options options("benchmark", "Benchmarking tool for NeuG"); options.add_options()("help", "Display help message")( "data-path,d", "", cxxopts::value())( "memory-level,m", "", cxxopts::value()->default_value("1"))( diff --git a/doc/source/reference/_meta.ts b/doc/source/reference/_meta.ts index 9fab6816..d4cb9464 100644 --- a/doc/source/reference/_meta.ts +++ b/doc/source/reference/_meta.ts @@ -1,4 +1,5 @@ export default { cpp_api: "C++ API", + java_api: "Java API", python_api: "Python API", }; diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst index ce1a1030..fe6e3bce 100644 --- a/doc/source/reference/index.rst +++ b/doc/source/reference/index.rst @@ -8,6 +8,7 @@ This section contains the complete API reference for NeuG. :caption: API Documentation Python API + Java API C++ API Python API @@ -15,6 +16,11 @@ Python API The Python API provides a high-level interface for interacting with NeuG databases. +Java API +-------- + +The Java API provides a Java-native driver interface for connecting to NeuG servers and executing queries. + C++ API ------- diff --git a/doc/source/reference/java_api/index.md b/doc/source/reference/java_api/index.md new file mode 100644 index 00000000..cf14b747 --- /dev/null +++ b/doc/source/reference/java_api/index.md @@ -0,0 +1,160 @@ +# Java API Reference + +The NeuG Java API provides a Java-native driver for connecting to NeuG servers, executing Cypher queries, and consuming typed query results. + +## Overview + +The Java driver is designed for application integration and service-side usage: + +- **Create drivers** to connect to a NeuG server over HTTP +- **Open sessions** to execute Cypher queries +- **Read results** through a typed `ResultSet` API +- **Inspect metadata** using native NeuG `Types` + +## Installation + +### Use from this repository + +```bash +cd tools/java_driver +mvn clean install -DskipTests +``` + +### Add dependency in another Maven project + +```xml + + com.alibaba.neug + neug-java-driver + 1.0.0-SNAPSHOT + +``` + +## Core Interfaces + +- **Driver** - manages connectivity and creates sessions +- **Session** - executes statements against a NeuG server +- **ResultSet** - reads rows and typed values from query results +- **ResultSetMetaData** - inspects result column names, nullability, and native NeuG types + +## Quick Start + +```java +import com.alibaba.neug.driver.Driver; +import com.alibaba.neug.driver.GraphDatabase; +import com.alibaba.neug.driver.ResultSet; +import com.alibaba.neug.driver.Session; + +public class Example { + public static void main(String[] args) { + try (Driver driver = GraphDatabase.driver("http://localhost:10000")) { + driver.verifyConnectivity(); + + try (Session session = driver.session(); + ResultSet rs = session.run("RETURN 1 AS value")) { + while (rs.next()) { + System.out.println(rs.getInt("value")); + } + } + } + } +} +``` + +## Configuration + +You can create a driver with custom connection settings through `Config`: + +```java +import com.alibaba.neug.driver.Driver; +import com.alibaba.neug.driver.GraphDatabase; +import com.alibaba.neug.driver.utils.Config; + +Config config = Config.builder() + .withConnectionTimeoutMillis(3000) + .build(); + +Driver driver = GraphDatabase.driver("http://localhost:10000", config); +``` + +## Parameterized Queries + +```java +import java.util.HashMap; +import java.util.Map; + +Map parameters = new HashMap<>(); +parameters.put("name", "Alice"); +parameters.put("age", 30); + +try (Session session = driver.session()) { + String query = "CREATE (p:Person {name: $name, age: $age}) RETURN p"; + try (ResultSet rs = session.run(query, parameters)) { + if (rs.next()) { + System.out.println(rs.getObject("p")); + } + } +} +``` + +## Reading Results + +The Java driver exposes typed accessors for common value types: + +- `getString(...)` +- `getInt(...)` +- `getLong(...)` +- `getBoolean(...)` +- `getDate(...)` +- `getTimestamp(...)` +- `getObject(...)` + +Example: + +```java +try (Session session = driver.session(); + ResultSet rs = session.run("MATCH (n:Person) RETURN n.name AS name, n.age AS age")) { + while (rs.next()) { + String name = rs.getString("name"); + int age = rs.getInt("age"); + System.out.println(name + ", " + age); + } +} +``` + +## Result Metadata + +`ResultSetMetaData` returns native NeuG types instead of JDBC SQL type codes. + +```java +ResultSetMetaData metaData = rs.getMetaData(); +String columnName = metaData.getColumnName(0); +Types columnType = metaData.getColumnType(0); +String typeName = metaData.getColumnTypeName(0); +``` + +This is useful when building higher-level abstractions on top of the driver or when dispatching logic based on result types. + +## Dependencies + +The Java driver depends on the following libraries: + +- OkHttp - HTTP client +- Protocol Buffers - response serialization +- Jackson - JSON processing +- SLF4J - logging facade + +These dependencies are managed automatically by Maven. + +## API Documentation + +- [Generated Javadoc](./apidocs/index.html) + +## Build Javadoc Locally + +```bash +cd tools/java_driver +mvn -DskipTests javadoc:javadoc +``` + +The generated Javadoc is written to `tools/java_driver/target/site/apidocs`. \ No newline at end of file From 9375a992c75b8a5399ef1384d3c79a7a2a4fdf54 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Mon, 16 Mar 2026 16:21:46 +0800 Subject: [PATCH 19/60] add maven --- .github/workflows/docs.yml | 2 +- .github/workflows/format-check.yml | 5 +++++ .github/workflows/neug-test.yml | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ae29a182..352515c6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -44,7 +44,7 @@ jobs: - name: Build Documentation run: | sudo apt update - sudo apt install -y doxygen graphviz + sudo apt install -y doxygen graphviz maven cd ${GITHUB_WORKSPACE}/tools/java_driver mvn -DskipTests javadoc:javadoc cd ${GITHUB_WORKSPACE}/tools/python_bind diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 85335108..cca46189 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -91,6 +91,11 @@ jobs: java-version: '17' cache: 'maven' + - name: Install Maven + run: | + sudo apt update + sudo apt install -y maven + - name: Get PR Changes uses: dorny/paths-filter@v3 id: changes diff --git a/.github/workflows/neug-test.yml b/.github/workflows/neug-test.yml index 3c771f0b..c70f5b58 100644 --- a/.github/workflows/neug-test.yml +++ b/.github/workflows/neug-test.yml @@ -142,6 +142,11 @@ jobs: java-version: '17' cache: 'maven' + - name: Install Maven + run: | + sudo apt update + sudo apt install -y maven + - name: Set ENABLE_GCOV for main branch if: steps.scope.outputs.extension_only != 'true' && (github.ref == 'refs/heads/main' && github.repository == 'alibaba/neug' && github.event_name == 'push') run: | From e8eeb5d2a0b13c77d4efb192dda8f75111710d84 Mon Sep 17 00:00:00 2001 From: liulx20 Date: Mon, 16 Mar 2026 16:47:37 +0800 Subject: [PATCH 20/60] Update tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../src/main/java/com/alibaba/neug/driver/utils/Client.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java index b4eaee51..f6f085f9 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java @@ -66,6 +66,9 @@ public Client(String uri, Config config) { * @throws IOException if an I/O error occurs during the request */ public byte[] syncPost(byte[] request) throws IOException { + if (closed) { + throw new IllegalStateException("Client is already closed"); + } RequestBody body = RequestBody.create(request); Request httpRequest = new Request.Builder().url(uri).post(body).build(); try (Response response = httpClient.newCall(httpRequest).execute()) { From b69967c1029031940316c9f875bd30a7da6a6b2b Mon Sep 17 00:00:00 2001 From: liulx20 Date: Mon, 16 Mar 2026 16:52:36 +0800 Subject: [PATCH 21/60] Update tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../com/alibaba/neug/driver/internal/InternalResultSet.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index e902ec00..b3a6900d 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -655,8 +655,11 @@ public void beforeFirst() { @Override public boolean first() { + if (response.getRowCount() == 0) { + return false; + } currentIndex = 0; - return currentIndex < response.getRowCount(); + return true; } @Override From 9362940ea383fa0d1a229712cbf003314c7b2413 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Mon, 16 Mar 2026 17:21:24 +0800 Subject: [PATCH 22/60] add e2e ci --- .github/workflows/neug-test.yml | 2 + .../neug/driver/JavaDriverE2ETest.java | 6 +- tools/python_bind/tests/test_java_driver.py | 60 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 tools/python_bind/tests/test_java_driver.py diff --git a/.github/workflows/neug-test.yml b/.github/workflows/neug-test.yml index c70f5b58..3446a5b0 100644 --- a/.github/workflows/neug-test.yml +++ b/.github/workflows/neug-test.yml @@ -358,6 +358,8 @@ jobs: run: | cd ${GITHUB_WORKSPACE}/tools/java_driver/ mvn test + cd ${GITHUB_WORKSPACE}/tools/python_bind/ + python3 -m pytest -sv tests/test_java_driver.py # ======================================== # Phase 4: E2E Tests diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java index d6e463ba..98277bb7 100644 --- a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java @@ -47,10 +47,10 @@ public void testDriverCanQueryLiveServer() { try (Session session = driver.session(); ResultSet resultSet = session.run("RETURN 1 AS value")) { assertTrue(resultSet.next()); - assertEquals(1, resultSet.getInt("value")); - assertEquals(1, resultSet.getObject(0)); + assertEquals(1L, resultSet.getLong("value")); + assertEquals(1L, resultSet.getObject(0)); assertFalse(resultSet.wasNull()); - assertEquals(Types.INT32, resultSet.getMetaData().getColumnType(0)); + assertEquals(Types.INT64, resultSet.getMetaData().getColumnType(0)); assertEquals("value", resultSet.getMetaData().getColumnName(0)); assertFalse(resultSet.next()); } diff --git a/tools/python_bind/tests/test_java_driver.py b/tools/python_bind/tests/test_java_driver.py new file mode 100644 index 00000000..9ae250b3 --- /dev/null +++ b/tools/python_bind/tests/test_java_driver.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import socket +import subprocess +import sys +import time + +sys.path.append(os.path.join(os.path.dirname(__file__), "../")) + +from neug.database import Database + + +def wait_until_ready(host, port, timeout=60): + deadline = time.time() + timeout + while time.time() < deadline: + try: + with socket.create_connection((host, port), timeout=1): + return + except OSError: + time.sleep(1) + raise RuntimeError(f"Timed out waiting for NeuG server on {host}:{port}") + + +def test_java_driver_e2e(): + host = os.environ.get("NEUG_JAVA_DRIVER_E2E_HOST", "127.0.0.1") + port = int(os.environ.get("NEUG_JAVA_DRIVER_E2E_PORT", "10010")) + db_path = os.environ.get("NEUG_JAVA_DRIVER_E2E_DB_PATH", "/tmp/modern_graph") + test_name = os.environ.get("NEUG_JAVA_DRIVER_E2E_TEST", "JavaDriverE2ETest") + repo_root = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "..") + ) + java_driver_dir = os.path.join(repo_root, "tools", "java_driver") + endpoint = f"http://{host}:{port}" + + db = Database(db_path=db_path, mode="w") + try: + db.serve(host=host, port=port, blocking=False) + wait_until_ready(host, port) + + env = os.environ.copy() + env["NEUG_JAVA_DRIVER_E2E_URI"] = endpoint + + result = subprocess.run( + ["mvn", "-q", f"-Dtest={test_name}", "test"], + cwd=java_driver_dir, + env=env, + check=False, + ) + assert result.returncode == 0 + finally: + try: + db.stop_serving() + except Exception: + pass + try: + db.close() + except Exception: + pass From 98736795b4120946ef4acb3c400de37da752ed06 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Mon, 16 Mar 2026 17:30:58 +0800 Subject: [PATCH 23/60] add param test --- .../neug/driver/JavaDriverE2ETest.java | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java index 98277bb7..efce5ab9 100644 --- a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java @@ -13,12 +13,11 @@ */ package com.alibaba.neug.driver; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import com.alibaba.neug.driver.utils.Types; +import com.alibaba.neug.driver.utils.*; +import java.util.Map; import org.junit.jupiter.api.Test; /** @@ -35,10 +34,15 @@ public class JavaDriverE2ETest { private static final String E2E_URI_ENV = "NEUG_JAVA_DRIVER_E2E_URI"; - @Test - public void testDriverCanQueryLiveServer() { + private static String requireE2EUri() { String uri = System.getenv(E2E_URI_ENV); assumeTrue(uri != null && !uri.isBlank(), E2E_URI_ENV + " is not set"); + return uri; + } + + @Test + public void testDriverCanQueryLiveServer() { + String uri = requireE2EUri(); try (Driver driver = GraphDatabase.driver(uri)) { assertFalse(driver.isClosed()); @@ -56,4 +60,26 @@ public void testDriverCanQueryLiveServer() { } } } + + @Test + public void testDriverCanRunParameterizedQuery() { + String uri = requireE2EUri(); + + try (Driver driver = GraphDatabase.driver(uri); + Session session = driver.session(); + ResultSet resultSet = + session.run( + "MATCH (n) WHERE n.name = $name RETURN n.age", + Map.of("name", "marko"), AccessMode.READ)) { + assertTrue(resultSet.next()); + assertEquals(29, resultSet.getInt(0)); + assertEquals(29, resultSet.getObject(0)); + assertFalse(resultSet.wasNull()); + assertEquals(Types.INT32, resultSet.getMetaData().getColumnType(0)); + assertEquals("_0_n.age", resultSet.getMetaData().getColumnName(0)); + assertFalse(resultSet.next()); + } + } + + } From 49fdfd495da65e6c81204d8e4e12448717709aa6 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Mon, 16 Mar 2026 17:32:52 +0800 Subject: [PATCH 24/60] format --- .../test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java index efce5ab9..76e05993 100644 --- a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java @@ -70,7 +70,8 @@ public void testDriverCanRunParameterizedQuery() { ResultSet resultSet = session.run( "MATCH (n) WHERE n.name = $name RETURN n.age", - Map.of("name", "marko"), AccessMode.READ)) { + Map.of("name", "marko"), + AccessMode.READ)) { assertTrue(resultSet.next()); assertEquals(29, resultSet.getInt(0)); assertEquals(29, resultSet.getObject(0)); @@ -80,6 +81,4 @@ public void testDriverCanRunParameterizedQuery() { assertFalse(resultSet.next()); } } - - } From d187d05b497422eb24966fc2a3c5a901db6db28e Mon Sep 17 00:00:00 2001 From: liulx20 Date: Mon, 16 Mar 2026 19:21:24 +0800 Subject: [PATCH 25/60] Update tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../com/alibaba/neug/driver/internal/InternalSession.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java index d183ade6..2b8d26d6 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java @@ -57,12 +57,18 @@ public ResultSet run(String query, AccessMode mode) { return run(query, null, mode); } + @Override @Override public ResultSet run(String query, Map parameters, AccessMode mode) { + if (closed) { + throw new IllegalStateException("Session is already closed"); + } try { byte[] request = QuerySerializer.serialize(query, parameters, mode); byte[] response = client.syncPost(request); return ResponseParser.parse(response); + } catch (IllegalStateException e) { + throw e; } catch (Exception e) { throw new RuntimeException("Failed to execute query", e); } From 4f9dec53423a3b2b75f0490b5854ad74bba08aa6 Mon Sep 17 00:00:00 2001 From: liulx20 Date: Mon, 16 Mar 2026 19:48:04 +0800 Subject: [PATCH 26/60] Update InternalSession.java --- .../java/com/alibaba/neug/driver/internal/InternalSession.java | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java index 2b8d26d6..f44e02cf 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java @@ -57,7 +57,6 @@ public ResultSet run(String query, AccessMode mode) { return run(query, null, mode); } - @Override @Override public ResultSet run(String query, Map parameters, AccessMode mode) { if (closed) { From 4b61c7f8f195c6c3f88179e34540db9824d4bdcc Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Mon, 16 Mar 2026 20:16:43 +0800 Subject: [PATCH 27/60] remove pb generated --- tools/java_driver/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/java_driver/pom.xml b/tools/java_driver/pom.xml index b397f9ab..208ea535 100644 --- a/tools/java_driver/pom.xml +++ b/tools/java_driver/pom.xml @@ -113,6 +113,11 @@ org.apache.maven.plugins maven-javadoc-plugin 3.5.0 + + + **/Results.java + + attach-javadocs From edccbb9cafa595a155165dbf49916bcc270315c8 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Mon, 16 Mar 2026 22:44:13 +0800 Subject: [PATCH 28/60] fix doc --- .github/workflows/docs.yml | 1 - doc/source/index.rst | 1 + doc/source/reference/_meta.ts | 2 +- doc/source/reference/java_api/_meta.ts | 8 ++ doc/source/reference/java_api/config.md | 45 +++++++++++ doc/source/reference/java_api/driver.md | 59 ++++++++++++++ doc/source/reference/java_api/index.md | 80 ++++++------------- doc/source/reference/java_api/result_set.md | 56 +++++++++++++ .../reference/java_api/result_set_metadata.md | 42 ++++++++++ doc/source/reference/java_api/session.md | 61 ++++++++++++++ 10 files changed, 296 insertions(+), 59 deletions(-) create mode 100644 doc/source/reference/java_api/_meta.ts create mode 100644 doc/source/reference/java_api/config.md create mode 100644 doc/source/reference/java_api/driver.md create mode 100644 doc/source/reference/java_api/result_set.md create mode 100644 doc/source/reference/java_api/result_set_metadata.md create mode 100644 doc/source/reference/java_api/session.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 352515c6..5ee00941 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -56,4 +56,3 @@ jobs: make html mkdir -p ${GITHUB_WORKSPACE}/doc/build/html/reference/java_api cp -r ${GITHUB_WORKSPACE}/tools/java_driver/target/site/apidocs ${GITHUB_WORKSPACE}/doc/build/html/reference/java_api/ - diff --git a/doc/source/index.rst b/doc/source/index.rst index 4dfcc1fb..71fb37aa 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -80,6 +80,7 @@ NeuG documentation Python API C++ API + Java API .. toctree:: :maxdepth: 1 diff --git a/doc/source/reference/_meta.ts b/doc/source/reference/_meta.ts index d4cb9464..1f49d889 100644 --- a/doc/source/reference/_meta.ts +++ b/doc/source/reference/_meta.ts @@ -1,5 +1,5 @@ export default { + python_api: "Python API", cpp_api: "C++ API", java_api: "Java API", - python_api: "Python API", }; diff --git a/doc/source/reference/java_api/_meta.ts b/doc/source/reference/java_api/_meta.ts new file mode 100644 index 00000000..388613ab --- /dev/null +++ b/doc/source/reference/java_api/_meta.ts @@ -0,0 +1,8 @@ +export default { + index: "Java API Overview", + driver: "Driver", + config: "Config", + session: "Session", + result_set: "ResultSet", + result_set_metadata: "ResultSetMetaData", +}; diff --git a/doc/source/reference/java_api/config.md b/doc/source/reference/java_api/config.md new file mode 100644 index 00000000..60040a29 --- /dev/null +++ b/doc/source/reference/java_api/config.md @@ -0,0 +1,45 @@ +# Config + +`Config` is used to customize Java driver behavior such as connection and timeout settings. + +## Purpose + +Use `Config` when you want to adjust driver-level HTTP behavior without changing application query code. + +Typical use cases include: + +- shorter connection timeouts in tests +- longer read timeouts for heavy queries +- tuning connection pool settings for service workloads + +## Basic Example + +```java +import com.alibaba.neug.driver.Driver; +import com.alibaba.neug.driver.GraphDatabase; +import com.alibaba.neug.driver.utils.Config; + +Config config = Config.builder() + .withConnectionTimeoutMillis(3000) + .build(); + +Driver driver = GraphDatabase.driver("http://localhost:10000", config); +``` + +## Common Options + +Depending on the driver version, `Config.Builder` can be used to tune: + +- connection timeout +- read timeout +- write timeout +- connection pool size +- keep-alive settings + +## Usage Notes + +- Create `Config` once and reuse it when constructing drivers +- Keep timeout values consistent with your deployment environment +- Prefer conservative defaults unless you have a specific performance reason to tune them + +See also: [Driver](driver) diff --git a/doc/source/reference/java_api/driver.md b/doc/source/reference/java_api/driver.md new file mode 100644 index 00000000..e31e3a39 --- /dev/null +++ b/doc/source/reference/java_api/driver.md @@ -0,0 +1,59 @@ +# Driver + +`Driver` is the main entry point for Java applications using NeuG. + +## Responsibilities + +- Create and own the underlying HTTP client +- Verify server connectivity +- Create `Session` instances +- Manage driver lifecycle through `close()` + +## Create a Driver + +```java +import com.alibaba.neug.driver.Driver; +import com.alibaba.neug.driver.GraphDatabase; + +Driver driver = GraphDatabase.driver("http://localhost:10000"); +``` + +## Create a Driver with Config + +```java +import com.alibaba.neug.driver.Driver; +import com.alibaba.neug.driver.GraphDatabase; +import com.alibaba.neug.driver.utils.Config; + +Config config = Config.builder() + .withConnectionTimeoutMillis(3000) + .build(); + +Driver driver = GraphDatabase.driver("http://localhost:10000", config); +``` + +## Verify Connectivity + +```java +try (Driver driver = GraphDatabase.driver("http://localhost:10000")) { + driver.verifyConnectivity(); +} +``` + +## Open Sessions + +```java +try (Driver driver = GraphDatabase.driver("http://localhost:10000")) { + try (Session session = driver.session()) { + // run queries here + } +} +``` + +## Lifecycle Notes + +- Reuse one `Driver` for multiple queries and sessions when possible +- Close the driver when the application shuts down +- `isClosed()` can be used to inspect driver state + +See also: [Session](session), [Generated Javadoc](apidocs/index.html) diff --git a/doc/source/reference/java_api/index.md b/doc/source/reference/java_api/index.md index cf14b747..28a2af62 100644 --- a/doc/source/reference/java_api/index.md +++ b/doc/source/reference/java_api/index.md @@ -2,6 +2,17 @@ The NeuG Java API provides a Java-native driver for connecting to NeuG servers, executing Cypher queries, and consuming typed query results. +```{toctree} +:maxdepth: 1 +:hidden: + +driver +config +session +result_set +result_set_metadata +``` + ## Overview The Java driver is designed for application integration and service-side usage: @@ -32,10 +43,11 @@ mvn clean install -DskipTests ## Core Interfaces -- **Driver** - manages connectivity and creates sessions -- **Session** - executes statements against a NeuG server -- **ResultSet** - reads rows and typed values from query results -- **ResultSetMetaData** - inspects result column names, nullability, and native NeuG types +- **[Driver](driver)** - manages connectivity and creates sessions +- **[Config](config)** - customizes connection and timeout behavior +- **[Session](session)** - executes statements against a NeuG server +- **[ResultSet](result_set)** - reads rows and typed values from query results +- **[ResultSetMetaData](result_set_metadata)** - inspects result column names, nullability, and native NeuG types ## Quick Start @@ -61,22 +73,6 @@ public class Example { } ``` -## Configuration - -You can create a driver with custom connection settings through `Config`: - -```java -import com.alibaba.neug.driver.Driver; -import com.alibaba.neug.driver.GraphDatabase; -import com.alibaba.neug.driver.utils.Config; - -Config config = Config.builder() - .withConnectionTimeoutMillis(3000) - .build(); - -Driver driver = GraphDatabase.driver("http://localhost:10000", config); -``` - ## Parameterized Queries ```java @@ -97,43 +93,13 @@ try (Session session = driver.session()) { } ``` -## Reading Results - -The Java driver exposes typed accessors for common value types: - -- `getString(...)` -- `getInt(...)` -- `getLong(...)` -- `getBoolean(...)` -- `getDate(...)` -- `getTimestamp(...)` -- `getObject(...)` - -Example: - -```java -try (Session session = driver.session(); - ResultSet rs = session.run("MATCH (n:Person) RETURN n.name AS name, n.age AS age")) { - while (rs.next()) { - String name = rs.getString("name"); - int age = rs.getInt("age"); - System.out.println(name + ", " + age); - } -} -``` - -## Result Metadata - -`ResultSetMetaData` returns native NeuG types instead of JDBC SQL type codes. - -```java -ResultSetMetaData metaData = rs.getMetaData(); -String columnName = metaData.getColumnName(0); -Types columnType = metaData.getColumnType(0); -String typeName = metaData.getColumnTypeName(0); -``` +## Reference Pages -This is useful when building higher-level abstractions on top of the driver or when dispatching logic based on result types. +- [Driver](driver) +- [Config](config) +- [Session](session) +- [ResultSet](result_set) +- [ResultSetMetaData](result_set_metadata) ## Dependencies @@ -148,7 +114,7 @@ These dependencies are managed automatically by Maven. ## API Documentation -- [Generated Javadoc](./apidocs/index.html) +- Generated Javadoc ## Build Javadoc Locally diff --git a/doc/source/reference/java_api/result_set.md b/doc/source/reference/java_api/result_set.md new file mode 100644 index 00000000..62f92bde --- /dev/null +++ b/doc/source/reference/java_api/result_set.md @@ -0,0 +1,56 @@ +# ResultSet + +`ResultSet` provides forward-only access to query results. + +## Common Access Pattern + +```java +try (Session session = driver.session(); + ResultSet rs = session.run("MATCH (n:Person) RETURN n.name AS name, n.age AS age")) { + while (rs.next()) { + String name = rs.getString("name"); + long age = rs.getLong("age"); + System.out.println(name + ", " + age); + } +} +``` + +## Typed Getters + +The Java driver exposes typed accessors for common value types: + +- `getString(...)` +- `getInt(...)` +- `getLong(...)` +- `getBoolean(...)` +- `getDate(...)` +- `getTimestamp(...)` +- `getObject(...)` + +## Access by Column Index + +```java +try (ResultSet rs = session.run("RETURN 1 AS value")) { + if (rs.next()) { + long value = rs.getLong(0); + Object raw = rs.getObject(0); + } +} +``` + +## Null Handling + +```java +Object value = rs.getObject(0); +boolean wasNull = rs.wasNull(); +``` + +## Metadata + +Each result set exposes metadata for column names and types: + +```java +ResultSetMetaData metaData = rs.getMetaData(); +``` + +See also: [ResultSetMetaData](result_set_metadata), [Session](session) diff --git a/doc/source/reference/java_api/result_set_metadata.md b/doc/source/reference/java_api/result_set_metadata.md new file mode 100644 index 00000000..64a99986 --- /dev/null +++ b/doc/source/reference/java_api/result_set_metadata.md @@ -0,0 +1,42 @@ +# ResultSetMetaData + +`ResultSetMetaData` describes the columns returned by a query. + +Unlike JDBC-oriented APIs, NeuG returns native driver `Types` instead of SQL type codes. + +## Example + +```java +ResultSetMetaData metaData = rs.getMetaData(); +String columnName = metaData.getColumnName(0); +Types columnType = metaData.getColumnType(0); +String typeName = metaData.getColumnTypeName(0); +``` + +## Common Methods + +- `getColumnCount(int)` +- `getColumnName(int)` +- `getColumnType(int)` +- `getColumnTypeName(int)` +- `isNullable(int)` +- `isSigned(int)` + +## Why Native Types + +The NeuG Java driver is not designed as a JDBC wrapper. Returning native `Types` makes it easier to: + +- preserve NeuG-specific type information +- avoid lossy JDBC mappings +- build driver-native abstractions on top of metadata + +## Example Dispatch + +```java +Types type = rs.getMetaData().getColumnType(0); +if (type == Types.INT64) { + long value = rs.getLong(0); +} +``` + +See also: [ResultSet](result_set) \ No newline at end of file diff --git a/doc/source/reference/java_api/session.md b/doc/source/reference/java_api/session.md new file mode 100644 index 00000000..ec58c002 --- /dev/null +++ b/doc/source/reference/java_api/session.md @@ -0,0 +1,61 @@ +# Session + +`Session` is the main query execution interface in the NeuG Java driver. + +## Responsibilities + +- Execute Cypher statements +- Send query parameters +- Select access mode when needed +- Return `ResultSet` objects for row-by-row reading + +## Basic Query Execution + +```java +try (Session session = driver.session(); + ResultSet rs = session.run("RETURN 1 AS value")) { + while (rs.next()) { + System.out.println(rs.getLong("value")); + } +} +``` + +## Parameterized Queries + +```java +import java.util.Map; + +try (Session session = driver.session(); + ResultSet rs = session.run( + "MATCH (n) WHERE n.name = $name RETURN n.age AS age", + Map.of("name", "marko"))) { + while (rs.next()) { + System.out.println(rs.getLong("age")); + } +} +``` + +## Access Modes + +```java +import com.alibaba.neug.driver.utils.AccessMode; +import java.util.Map; + +try (Session session = driver.session(); + ResultSet rs = session.run( + "MATCH (n) WHERE n.age > $age RETURN n", + Map.of("age", 30), + AccessMode.READ)) { + while (rs.next()) { + System.out.println(rs.getObject("n")); + } +} +``` + +## Usage Notes + +- `Session` is lightweight and intended for short-lived use +- Use try-with-resources to ensure it is closed cleanly +- Each `run(...)` call returns a `ResultSet` that should also be closed + +See also: [Driver](driver), [ResultSet](result_set) From 97dc68a88cd95dcf8e5951498b2dbc3ee736b313 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Mon, 16 Mar 2026 22:51:04 +0800 Subject: [PATCH 29/60] fix doc --- doc/source/reference/java_api/driver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/reference/java_api/driver.md b/doc/source/reference/java_api/driver.md index e31e3a39..84d8b841 100644 --- a/doc/source/reference/java_api/driver.md +++ b/doc/source/reference/java_api/driver.md @@ -56,4 +56,4 @@ try (Driver driver = GraphDatabase.driver("http://localhost:10000")) { - Close the driver when the application shuts down - `isClosed()` can be used to inspect driver state -See also: [Session](session), [Generated Javadoc](apidocs/index.html) +See also: [Session](session) From 74b5c76e875d77799679f959387ff105cbab5a35 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Tue, 17 Mar 2026 16:04:34 +0800 Subject: [PATCH 30/60] fix doc --- doc/source/reference/java_api/index.md | 78 +++++++++++++++++++ doc/source/reference/java_api/index.rst | 12 +++ .../neug/driver/JavaDriverE2ETest.java | 2 +- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 doc/source/reference/java_api/index.rst diff --git a/doc/source/reference/java_api/index.md b/doc/source/reference/java_api/index.md index 28a2af62..89ee3b6e 100644 --- a/doc/source/reference/java_api/index.md +++ b/doc/source/reference/java_api/index.md @@ -22,6 +22,15 @@ The Java driver is designed for application integration and service-side usage: - **Read results** through a typed `ResultSet` API - **Inspect metadata** using native NeuG `Types` +## Deployment Model + +The current Java SDK supports **remote access over HTTP only**. + +- **Supported**: connect to a running NeuG server with `GraphDatabase.driver("http://host:port")` +- **Not supported**: embedded/in-process database access from Java + +If you need embedded access, use the C++ or Python APIs. The Java SDK should be treated as a client for an already running NeuG service. + ## Installation ### Use from this repository @@ -73,6 +82,75 @@ public class Example { } ``` +## Start a NeuG Server + +Before using the Java SDK, start a NeuG HTTP server that exposes the query endpoint. +You can use either the C++ binary or the Python API to start the server. + +### Option A: Start with the C++ binary + +#### 1. Build the server binary + +From the repository root: + +```bash +cmake -S . -B build +cmake --build build --target rt_server -j +``` + +#### 2. Start the server + +```bash +./build/bin/rt_server --data-path /path/to/graph --http-port 10000 --host 0.0.0.0 --shard-num 16 +``` + +Common options: + +- `--data-path`: path to the NeuG data directory +- `--http-port`: HTTP port for Java clients, default is `10000` +- `--host`: bind address, default is `127.0.0.1` +- `--shard-num`: shard number of actor system, default is `9` + +### Option B: Start with Python + +If you have the `neug` Python package installed, you can start the server directly from Python: + +```python +from neug import Database + +db = Database("/path/to/graph", mode="rw") +# Blocks until the process is killed (Ctrl+C or SIGTERM) +db.serve(port=10000, host="0.0.0.0", blocking=True) +``` + +To run non-blocking (e.g. inside a larger script): + +```python +import time +from neug import Database + +db = Database("/path/to/graph", mode="rw") +uri = db.serve(port=10000, host="0.0.0.0", blocking=False) +print("Server started at:", uri) + +try: + while True: + time.sleep(60) +except KeyboardInterrupt: + db.stop_serving() +``` + +> **Note:** Make sure all local connections are closed before calling `db.serve()`. +> Once the server is running, no new local connections are allowed until `db.stop_serving()` is called. + +### Connect from Java + +After the server is started via either option: + +```java +Driver driver = GraphDatabase.driver("http://localhost:10000"); +``` + ## Parameterized Queries ```java diff --git a/doc/source/reference/java_api/index.rst b/doc/source/reference/java_api/index.rst new file mode 100644 index 00000000..5cb4fe37 --- /dev/null +++ b/doc/source/reference/java_api/index.rst @@ -0,0 +1,12 @@ +Java API Reference +================== + +.. toctree:: + :maxdepth: 2 + :caption: Java API + + driver + config + session + result_set + result_set_metadata diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java index 76e05993..5f142d3a 100644 --- a/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java @@ -51,7 +51,7 @@ public void testDriverCanQueryLiveServer() { try (Session session = driver.session(); ResultSet resultSet = session.run("RETURN 1 AS value")) { assertTrue(resultSet.next()); - assertEquals(1L, resultSet.getLong("value")); + assertEquals(1, resultSet.getInt("value")); assertEquals(1L, resultSet.getObject(0)); assertFalse(resultSet.wasNull()); assertEquals(Types.INT64, resultSet.getMetaData().getColumnType(0)); From 94e8ba950765d8de11fad1bd3519eef8b32295bc Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Tue, 17 Mar 2026 16:20:52 +0800 Subject: [PATCH 31/60] fix workflows --- .github/workflows/docs.yml | 10 ++++------ .github/workflows/format-check.yml | 12 +++--------- .github/workflows/neug-test.yml | 12 +++--------- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5ee00941..c2dfa049 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,18 +33,16 @@ jobs: with: python-version: '3.10' - - name: Set up Java - uses: actions/setup-java@v4 + - name: Setup Java and Maven + uses: s4u/setup-maven-action@v1 with: - distribution: 'temurin' java-version: '17' - cache: 'maven' - + maven-version: '3.9.6' - name: Build Documentation run: | sudo apt update - sudo apt install -y doxygen graphviz maven + sudo apt install -y doxygen graphviz cd ${GITHUB_WORKSPACE}/tools/java_driver mvn -DskipTests javadoc:javadoc cd ${GITHUB_WORKSPACE}/tools/python_bind diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index cca46189..6b2e9dc3 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -84,17 +84,11 @@ jobs: with: python-version: '3.10' - - name: Set up Java - uses: actions/setup-java@v4 + - name: Setup Java and Maven + uses: s4u/setup-maven-action@v1 with: - distribution: 'temurin' java-version: '17' - cache: 'maven' - - - name: Install Maven - run: | - sudo apt update - sudo apt install -y maven + maven-version: '3.9.6' - name: Get PR Changes uses: dorny/paths-filter@v3 diff --git a/.github/workflows/neug-test.yml b/.github/workflows/neug-test.yml index a6161576..3910326f 100644 --- a/.github/workflows/neug-test.yml +++ b/.github/workflows/neug-test.yml @@ -135,17 +135,11 @@ jobs: with: python-version: '3.13' - - name: Setup Java - uses: actions/setup-java@v4 + - name: Setup Java and Maven + uses: s4u/setup-maven-action@v1 with: - distribution: 'temurin' java-version: '17' - cache: 'maven' - - - name: Install Maven - run: | - sudo apt update - sudo apt install -y maven + maven-version: '3.9.6' - name: Set ENABLE_GCOV for main branch if: steps.scope.outputs.extension_only != 'true' && (github.ref == 'refs/heads/main' && github.repository == 'alibaba/neug' && github.event_name == 'push') From fccbcf45235698c6e008d409a71ae9dcab963738 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Tue, 17 Mar 2026 16:28:34 +0800 Subject: [PATCH 32/60] fix version --- .github/workflows/docs.yml | 2 +- .github/workflows/format-check.yml | 2 +- .github/workflows/neug-test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c2dfa049..6172aa3a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,7 +34,7 @@ jobs: python-version: '3.10' - name: Setup Java and Maven - uses: s4u/setup-maven-action@v1 + uses: s4u/setup-maven-action@v1.5.0 with: java-version: '17' maven-version: '3.9.6' diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index c3768fcd..dad16cb1 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -91,7 +91,7 @@ jobs: python-version: '3.10' - name: Setup Java and Maven - uses: s4u/setup-maven-action@v1 + uses: s4u/setup-maven-action@v1.5.0 with: java-version: '17' maven-version: '3.9.6' diff --git a/.github/workflows/neug-test.yml b/.github/workflows/neug-test.yml index 7889aa33..9d03a1f6 100644 --- a/.github/workflows/neug-test.yml +++ b/.github/workflows/neug-test.yml @@ -144,7 +144,7 @@ jobs: python-version: '3.13' - name: Setup Java and Maven - uses: s4u/setup-maven-action@v1 + uses: s4u/setup-maven-action@v1.5.0 with: java-version: '17' maven-version: '3.9.6' From edfaa631ce97ad46cd98539e0e4264b010c6d8a2 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Tue, 17 Mar 2026 16:43:08 +0800 Subject: [PATCH 33/60] fix generator --- doc/source/_scripts/generate_cpp_docs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/_scripts/generate_cpp_docs.py b/doc/source/_scripts/generate_cpp_docs.py index a5440eb6..d202ead7 100644 --- a/doc/source/_scripts/generate_cpp_docs.py +++ b/doc/source/_scripts/generate_cpp_docs.py @@ -1890,6 +1890,7 @@ def _generate_reference_meta_file(self): content = """export default { cpp_api: "C++ API", python_api: "Python API", + java_api: "Java API", }; """ From 32419620578d24af965e15e28ccb14892bd2bb7b Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Tue, 17 Mar 2026 16:54:32 +0800 Subject: [PATCH 34/60] fix maven action --- .github/workflows/docs.yml | 2 +- .github/workflows/format-check.yml | 2 +- .github/workflows/neug-test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6172aa3a..ef072c43 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,7 +34,7 @@ jobs: python-version: '3.10' - name: Setup Java and Maven - uses: s4u/setup-maven-action@v1.5.0 + uses: s4u/setup-maven-action@v1.19.0 with: java-version: '17' maven-version: '3.9.6' diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index dad16cb1..9d6857db 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -91,7 +91,7 @@ jobs: python-version: '3.10' - name: Setup Java and Maven - uses: s4u/setup-maven-action@v1.5.0 + uses: s4u/setup-maven-action@v1.19.0 with: java-version: '17' maven-version: '3.9.6' diff --git a/.github/workflows/neug-test.yml b/.github/workflows/neug-test.yml index 9d03a1f6..a5d746ab 100644 --- a/.github/workflows/neug-test.yml +++ b/.github/workflows/neug-test.yml @@ -144,7 +144,7 @@ jobs: python-version: '3.13' - name: Setup Java and Maven - uses: s4u/setup-maven-action@v1.5.0 + uses: s4u/setup-maven-action@v1.19.0 with: java-version: '17' maven-version: '3.9.6' From 016555afcb815a8baf88da87ebabf749006fe046 Mon Sep 17 00:00:00 2001 From: Longbin Lai Date: Tue, 17 Mar 2026 17:53:06 +0800 Subject: [PATCH 35/60] fix: catch OSError in neug-cli readline history loading on macOS (#75) * fix: catch OSError in neug-cli readline history loading on macOS On macOS, Python's readline module is backed by libedit instead of GNU readline. When ~/.neug_history was written by a GNU readline session (e.g. from Docker/Linux), libedit raises OSError (errno 22 EINVAL) instead of silently handling the incompatible format. The original code only caught FileNotFoundError, causing neug-cli to crash on startup. Broaden the exception handler to also catch OSError so the history file is simply skipped, matching the intended behavior. Fixes #74 * fix: scope OSError catch to errno.EINVAL for libedit incompatibility Per greptile review: catching the full OSError base class could silently swallow unrelated errors such as PermissionError or IsADirectoryError. Narrow the catch to only suppress errno.EINVAL (22), which is the specific error raised by macOS libedit when it encounters a GNU readline history file. All other OSError variants are re-raised so users see genuine problems. Also add 'import errno' to top-level imports. --- tools/python_bind/neug/neug_cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/python_bind/neug/neug_cli.py b/tools/python_bind/neug/neug_cli.py index 01bed818..64980375 100644 --- a/tools/python_bind/neug/neug_cli.py +++ b/tools/python_bind/neug/neug_cli.py @@ -18,6 +18,7 @@ import atexit import cmd +import errno import logging import os import re @@ -70,6 +71,13 @@ def __init__(self, connection): readline.read_history_file(self._histfile) except FileNotFoundError: pass + except OSError as e: + # OSError (errno 22/EINVAL): libedit (macOS) cannot parse a + # GNU readline history file. Safe to ignore. + # Re-raise for any other OS error (e.g. EPERM) so unexpected + # problems still surface to the user. + if e.errno != errno.EINVAL: + raise atexit.register(self._save_history, self._histfile) else: logger.info("Command history disabled; readline support not detected.") From 82d8330de4d954a73a8967b5b525855be49845ec Mon Sep 17 00:00:00 2001 From: liulx20 Date: Tue, 17 Mar 2026 18:51:54 +0800 Subject: [PATCH 36/60] Update tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalDriver.java Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../java/com/alibaba/neug/driver/internal/InternalDriver.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalDriver.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalDriver.java index b354411f..9223066c 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalDriver.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalDriver.java @@ -42,6 +42,9 @@ public InternalDriver(String uri, Config config) { @Override public Session session() { + if (client.isClosed()) { + throw new IllegalStateException("Driver is already closed"); + } return new InternalSession(client); } From d914a308589bad34c59ca07748b62ca952f38d96 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Tue, 17 Mar 2026 18:53:57 +0800 Subject: [PATCH 37/60] fix getBigDecimal --- .../com/alibaba/neug/driver/internal/InternalResultSet.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index b3a6900d..891f1ce3 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -511,9 +511,6 @@ public BigDecimal getBigDecimal(int columnIndex) { checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); Number value = getNumericValue(arr); - if (value == null) { - return null; - } if (value instanceof BigInteger) { return new BigDecimal((BigInteger) value); } else if (value instanceof Integer || value instanceof Long) { From e3f104f57fa5631bf50972d0e5f892c5dd0f99c6 Mon Sep 17 00:00:00 2001 From: liulx20 Date: Tue, 17 Mar 2026 19:09:07 +0800 Subject: [PATCH 38/60] Update tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../com/alibaba/neug/driver/internal/InternalResultSet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index 891f1ce3..37eec013 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -75,12 +75,12 @@ public boolean next() { return false; } - @Override public boolean previous() { if (currentIndex - 1 >= 0) { currentIndex--; return true; } + currentIndex = -1; // move to before-first position return false; } From 1c8e76f75b3a51d71eaef13747cf6b8d787cd71a Mon Sep 17 00:00:00 2001 From: liulx20 Date: Tue, 17 Mar 2026 19:09:28 +0800 Subject: [PATCH 39/60] Update tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../java/com/alibaba/neug/driver/internal/InternalResultSet.java | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index 37eec013..2665aea0 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -72,6 +72,7 @@ public boolean next() { currentIndex++; return true; } + currentIndex = response.getRowCount(); // move to after-last position return false; } From 972c487f24ccaa243c1320ad468a04054c679464 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Tue, 17 Mar 2026 19:20:16 +0800 Subject: [PATCH 40/60] fix getObject --- .../neug/driver/internal/InternalResultSet.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index 891f1ce3..685471d7 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -154,6 +154,17 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH } return array.getBoolArray().getValues(rowIndex); } + case FLOAT_ARRAY: + { + if (!nullAlreadyHandled) { + ByteString nullBitmap = array.getFloatArray().getValidity(); + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) + == 0; + } + return array.getFloatArray().getValues(rowIndex); + } case DOUBLE_ARRAY: { if (!nullAlreadyHandled) { From 25a83f83ab14107f2f361bb8b0f842999510e7ae Mon Sep 17 00:00:00 2001 From: Xiaoli Zhou Date: Wed, 18 Mar 2026 12:25:30 +0800 Subject: [PATCH 41/60] feat: Support Export Query Results to JSON/JSONL file (#60) * support export arrow table to csv format Committed-by: Xiaoli Zhou from Dev container * export query response PB to csv format Committed-by: Xiaoli Zhou from Dev container * minor fix according to review Committed-by: Xiaoli Zhou from Dev container * fix according to review Committed-by: Xiaoli Zhou from Dev container * minor fix Committed-by: Xiaoli Zhou from Dev container * support export query results to json format Committed-by: Xiaoli Zhou from Dev container * minor fix Committed-by: Xiaoli Zhou from Dev container * remove 'newline_delimited' settings and detect jsonl format from path Committed-by: Xiaoli Zhou from Dev container Committed-by: Xiaoli Zhou from Dev container Committed-by: Xiaoli Zhou from Dev container Committed-by: Xiaoli Zhou from Dev container * minor fix Committed-by: Xiaoli Zhou from Dev container * add export to json tests in CI Committed-by: Xiaoli Zhou from Dev container Committed-by: Xiaoli Zhou from Dev container * Update extension/json/src/json_export_function.cc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update extension/json/src/json_export_function.cc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update extension/json/src/json_export_function.cc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * minor fix Committed-by: Xiaoli Zhou from Dev container * minor fix Committed-by: Xiaoli Zhou from Dev container * refine extension tests anotation Committed-by: Xiaoli Zhou from Dev container * minor fix Committed-by: Xiaoli Zhou from Dev container * rename INSTALL_EXTENSIONS to CI_INSTALL_EXTENSIONS to avoid conflict Committed-by: Xiaoli Zhou from Dev container * refine json extension tests ci Committed-by: Xiaoli Zhou from Dev container * minor fix Committed-by: Xiaoli Zhou from Dev container Committed-by: Xiaoli Zhou from Dev container --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .github/workflows/neug-extension-test.yml | 172 ++++++- doc/source/extensions/load_json.md | 72 ++- extension/json/include/json_export_function.h | 102 ++++ extension/json/include/json_options.h | 2 - extension/json/include/json_read_function.h | 68 ++- extension/json/src/json_dataset_builder.cc | 38 -- extension/json/src/json_export_function.cc | 487 ++++++++++++++++++ extension/json/src/json_extension.cpp | 11 +- include/neug/utils/reader/schema.h | 1 + include/neug/utils/writer/writer.h | 8 +- tests/unittest/test_extension.cc | 2 +- tools/python_bind/example/complex_test.py | 80 ++- tools/python_bind/pyproject.toml | 2 +- tools/python_bind/setup.py | 59 ++- tools/python_bind/tests/test_export.py | 201 ++++++++ tools/python_bind/tests/test_load.py | 34 +- 16 files changed, 1228 insertions(+), 111 deletions(-) create mode 100644 extension/json/include/json_export_function.h create mode 100644 extension/json/src/json_export_function.cc diff --git a/.github/workflows/neug-extension-test.yml b/.github/workflows/neug-extension-test.yml index 6ae62f2f..383964cf 100644 --- a/.github/workflows/neug-extension-test.yml +++ b/.github/workflows/neug-extension-test.yml @@ -1,6 +1,9 @@ name: NeuG Extension Test on: + schedule: + # Run at 08:00 UTC every Saturday + - cron: '0 8 * * 6' workflow_dispatch: # Manual trigger (must exist on default branch to see "Run workflow") inputs: branch: @@ -8,6 +11,11 @@ on: required: true default: 'main' type: string + rebuild_neug: + description: 'Rebuild NeuG wheel package before testing extension' + required: false + default: false + type: boolean concurrency: group: ${{ github.repository }}-${{ github.event.number || github.head_ref || github.sha }}-${{ github.workflow }} @@ -17,7 +25,7 @@ jobs: # ============================================================ # Job 1: Build NeuG with extensions # ============================================================ - build: + extension_tests_default: runs-on: [self-hosted] container: image: neug-registry.cn-hongkong.cr.aliyuncs.com/neug/neug-dev:v0.1.0 @@ -109,6 +117,164 @@ jobs: pip3 install -r requirements.txt pip3 install -r requirements_dev.txt export FLEX_DATA_DIR=${GITHUB_WORKSPACE}/example_dataset/tinysnb - export NEUG_RUN_JSON_TESTS=true GLOG_v=10 ./build/neug_py_bind/tools/utils/bulk_loader -g ../../example_dataset/tinysnb/graph.yaml -l ../../example_dataset/tinysnb/import.yaml -d /tmp/tinysnb - python3 -m pytest -sv tests/test_load.py -k "json" \ No newline at end of file + export FLEX_DATA_DIR=${GITHUB_WORKSPACE}/example_dataset/comprehensive_graph + GLOG_v=10 ./build/neug_py_bind/tools/utils/bulk_loader -g ../../example_dataset/comprehensive_graph/graph.yaml -l ../../example_dataset/comprehensive_graph/import.yaml -d /tmp/comprehensive_graph + NEUG_RUN_EXTENSION_TESTS=true python3 -m pytest -sv tests/test_load.py -k "json" + NEUG_RUN_EXTENSION_TESTS=true python3 -m pytest -sv tests/test_export.py -k "json" + + extension_tests_wheel_linux_x86_64: + runs-on: [self-hosted, linux, x64] + steps: + - name: checkout + id: checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Setup Python Env + run: | + USER_SITE=$(python3 -c "import site; print(site.getusersitepackages())") + echo "USER_SITE=$USER_SITE" >> $GITHUB_ENV + + - name: Install cibuildwheel + run: python3 -m pip install cibuildwheel==2.23.3 + + - name: Prepare for linux + run: | + echo "CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)" >> $GITHUB_ENV + + - name: Limit builds to CPython 3.13 on PRs + run: | + echo "CIBW_SKIP=cp39-* cp38-* cp311-* cp312-* cp310-*" >> $GITHUB_ENV + echo "CIBW_BUILD=cp313-manylinux_x86_64" >> $GITHUB_ENV + + - name: Build NeuG wheel + if: inputs.rebuild_neug == true + run: | + export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) + export BUILD_TYPE=RELEASE + export PYBIND11_FINDPYTHON=OFF + python3 -m cibuildwheel ./tools/python_bind --output-dir wheelhouse + + - name: Build Extension Package + run: | + export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) + export BUILD_TYPE=RELEASE + export PYBIND11_FINDPYTHON=OFF + CI_INSTALL_EXTENSIONS="json" python3 -m cibuildwheel ./tools/python_bind --output-dir extension_packages + + - name: Clean all installed python packages + run: | + python3 -m pip freeze | xargs python3 -m pip uninstall -y || true + + - name: Install Wheel and extension + run: | + python3 -m pip install --upgrade pip + python3 -m pip uninstall -y neug || echo "Uninstalled neug if it was installed" + # install NeuG wheel + if [ "${{ inputs.rebuild_neug }}" = "true" ]; then + for i in wheelhouse/*.whl; do + [ -f "$i" ] && python3 -m pip install "$i" || echo "Failed to install $i" + done + else + python3 -m pip install neug + fi + # install Extension Package (extract first .whl when multiple exist) + EXT_WHL=$(echo extension_packages/*.whl | awk '{print $1}') + python3 -m zipfile -e "$EXT_WHL" extension_packages + # get neug install directory (site-packages) and install extension there + rm -rf $USER_SITE/extension + cp -r extension_packages/extension $USER_SITE + + - name: Run simple and complex tests + env: + TMP_DIR: ${{ github.workspace }}/${{ github.run_id }} + run: | + rm -rf ${TMP_DIR}/test_example + GLOG_v=10 python3 tools/python_bind/example/simple_example.py example_dataset/modern_graph ${TMP_DIR}/test_example + NEUG_RUN_EXTENSION_TESTS=1 python3 tools/python_bind/example/complex_test.py + + extension_tests_wheel_linux_arm64: + runs-on: [self-hosted, linux, arm64] + steps: + - name: checkout + id: checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Python 3.13 + if: runner.name != 'arm64-1' + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Setup Python Env + run: | + USER_SITE=$(python3 -c "import site; print(site.getusersitepackages())") + echo "USER_SITE=$USER_SITE" >> $GITHUB_ENV + echo "PYTHONPATH=$USER_SITE${PYTHONPATH:+:$PYTHONPATH}" >> $GITHUB_ENV + + - name: Install cibuildwheel + run: python3 -m pip install cibuildwheel==2.23.3 + + - name: Prepare for linux + run: | + echo "CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)" >> $GITHUB_ENV + + - name: Limit builds to CPython 3.13 on PRs + run: | + echo "CIBW_SKIP=cp39-* cp38-* cp311-* cp312-* cp310-*" >> $GITHUB_ENV + echo "CIBW_BUILD=cp313-manylinux_aarch64" >> $GITHUB_ENV + + - name: Build NeuG wheel + if: inputs.rebuild_neug == true + run: | + export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) + export BUILD_TYPE=RELEASE + export PYBIND11_FINDPYTHON=OFF + python3 -m cibuildwheel ./tools/python_bind --output-dir wheelhouse + + - name: Build Extension Package + run: | + export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) + export BUILD_TYPE=RELEASE + export PYBIND11_FINDPYTHON=OFF + CI_INSTALL_EXTENSIONS="json" python3 -m cibuildwheel ./tools/python_bind --output-dir extension_packages + + - name: Clean all installed python packages + run: | + python3 -m pip freeze | xargs python3 -m pip uninstall -y || true + + - name: Install Wheel and extension + run: | + python3 -m pip install --upgrade pip + python3 -m pip uninstall -y neug || echo "Uninstalled neug if it was installed" + # install NeuG wheel + if [ "${{ inputs.rebuild_neug }}" = "true" ]; then + for i in wheelhouse/*.whl; do + [ -f "$i" ] && python3 -m pip install "$i" || echo "Failed to install $i" + done + else + python3 -m pip install neug + fi + # install Extension Package (extract first .whl when multiple exist) + EXT_WHL=$(echo extension_packages/*.whl | awk '{print $1}') + python3 -m zipfile -e "$EXT_WHL" extension_packages + # get neug install directory (site-packages) and install extension there + rm -rf $USER_SITE/extension + cp -r extension_packages/extension $USER_SITE + + - name: Run simple and complex tests + env: + TMP_DIR: ${{ github.workspace }}/${{ github.run_id }} + run: | + rm -rf ${TMP_DIR}/test_example + GLOG_v=10 python3 tools/python_bind/example/simple_example.py example_dataset/modern_graph ${TMP_DIR}/test_example + NEUG_RUN_EXTENSION_TESTS=1 python3 tools/python_bind/example/complex_test.py diff --git a/doc/source/extensions/load_json.md b/doc/source/extensions/load_json.md index a489f583..be994ef6 100644 --- a/doc/source/extensions/load_json.md +++ b/doc/source/extensions/load_json.md @@ -1,6 +1,6 @@ # JSON Extension -JSON (JavaScript Object Notation) is a widely used data format for web APIs and data exchange. NeuG supports JSON file import functionality through the Extension framework. After loading the JSON Extension, users can directly load external JSON files using the `LOAD FROM` syntax. +JSON (JavaScript Object Notation) is a widely used data format for web APIs and data exchange. NeuG supports JSON file import functionality through the Extension framework. After loading the JSON Extension, users can directly load external JSON files using the `LOAD FROM` syntax, or export query results to JSON files using the `COPY TO` syntax. ## Install Extension @@ -16,17 +16,14 @@ LOAD JSON; ## Using JSON Extension -`LOAD FROM` supports two JSON formats: **JSON Array** and **JSONL** (JSON Lines). - -### JSON Format Options - -The following options control how JSON files are parsed: +### Supported Formats -| Option | Type | Default | Description | -| ------------------- | ---- | ------- | -------------------------------------------------------------------------------------------- | -| `newline_delimited` | bool | `false` | If `true`, treats the file as JSONL format (one JSON object per line). If `false`, treats the file as a JSON array. | +Both import (`LOAD FROM`) and export (`COPY TO`) support two JSON formats: **JSON Array** and **JSONL** (JSON Lines). The format is inferred automatically from the file extension, so no explicit configuration is required. -### Supported Formats +| Extension | Format | Description | +| --------- | ------ | ----------- | +| `.json` | JSON array | One JSON array containing all result rows as objects. | +| `.jsonl` | JSON Lines | One JSON object per line (same as the JSONL import format). | #### JSON Array Format @@ -39,7 +36,7 @@ A JSON array contains multiple objects in a single array structure: ] ``` -When `newline_delimited` is `false` (default), the system parses the entire file as a single JSON array. +For paths with a `.json` extension (e.g. `person.json`), NeuG automatically treats the file as a JSON array for both import and export. #### JSONL Format (JSON Lines) @@ -50,9 +47,9 @@ JSONL format contains one JSON object per line: {"id": 2, "name": "Bob", "age": 25} ``` -When `newline_delimited` is `true`, the system parses each line as a separate JSON object. This format is particularly efficient for large datasets as it enables streaming processing. +For paths with a `.jsonl` extension (e.g. `person.jsonl`), NeuG automatically treats the file as JSONL (one JSON object per line) for both import and export. -### Query Examples +### Load from JSON #### Basic JSON Array Loading @@ -65,10 +62,10 @@ RETURN *; #### JSONL Format Loading -Load data from a JSONL file by specifying `newline_delimited=true`: +Load data from a JSONL file. When the path has a `.jsonl` extension, the format is auto-detected; ```cypher -LOAD FROM "person.jsonl" (newline_delimited=true) +LOAD FROM "person.jsonl" RETURN *; ``` @@ -77,7 +74,7 @@ RETURN *; Return only specific columns from JSON data: ```cypher -LOAD FROM "person.jsonl" (newline_delimited=true) +LOAD FROM "person.jsonl" RETURN fName, age; ``` @@ -86,8 +83,49 @@ RETURN fName, age; Use `AS` to assign aliases to columns: ```cypher -LOAD FROM "person.jsonl" (newline_delimited=true) +LOAD FROM "person.jsonl" RETURN fName AS name, age AS years; ``` > **Note:** All relational operations supported by `LOAD FROM` — including type conversion, WHERE filtering, aggregation, sorting, and limiting — work the same way with JSON files. See the [LOAD FROM reference](../data_io/load_data) for the complete list of operations. + +### Export to JSON + +With the JSON extension loaded, you can export query results to JSON or JSONL using the `COPY TO` syntax. + +#### Export as JSON Array + +Export the result of a query to a single JSON array file: + +```cypher +COPY (MATCH (p:person) RETURN p.*) TO 'person.json'; +``` + +This produces a file such as: + +```json +[{"id": 1, "name": "marko", "age": 29},{"id": 2, "name": "vadas", "age": 27}] +``` + +#### Export as JSONL + +Export to JSONL (one object per line) by using a `.jsonl` path: + +```cypher +COPY (MATCH (p:person) RETURN p.*) TO 'person.jsonl'; +``` + +Example output: + +```jsonl +{"id": 1, "name": "marko", "age": 29} +{"id": 2, "name": "vadas", "age": 27} +``` + +JSONL is well-suited for large result sets and streaming. You can control how many rows are written per batch with the `BATCH_SIZE` parameter: + +| Parameter | Description | Default | +| ----------- | --------------------------------------------------------- | ------- | +| `BATCH_SIZE` | Maximum number of rows to write in a single batch. | `1024` | + +For more on export options and best practices, see [Export Data](../data_io/export_data). diff --git a/extension/json/include/json_export_function.h b/extension/json/include/json_export_function.h new file mode 100644 index 00000000..cb63f176 --- /dev/null +++ b/extension/json/include/json_export_function.h @@ -0,0 +1,102 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "neug/compiler/function/export/export_function.h" +#include "neug/utils/result.h" +#include "neug/utils/writer/writer.h" +#include "rapidjson/document.h" + +namespace neug { +namespace writer { + +static constexpr const char* DEFAULT_JSON_NEWLINE = "\n"; +class JsonArrayStringFormatBuffer : public StringFormatBuffer { + public: + JsonArrayStringFormatBuffer(const neug::QueryResponse* response, + const reader::FileSchema& schema, + const reader::EntrySchema& entry_schema); + ~JsonArrayStringFormatBuffer() = default; + void addValue(int rowIdx, int colIdx) override; + neug::Status flush(std::shared_ptr stream) override; + + private: + const reader::EntrySchema& entry_schema_; + rapidjson::Value current_line_; + rapidjson::Value buffer_; + // get allocator from document + rapidjson::Document document_; +}; + +class JsonLStringFormatBuffer : public StringFormatBuffer { + public: + JsonLStringFormatBuffer(const neug::QueryResponse* response, + const reader::FileSchema& schema, + const reader::EntrySchema& entry_schema); + ~JsonLStringFormatBuffer() = default; + void addValue(int rowIdx, int colIdx) override; + neug::Status flush(std::shared_ptr stream) override; + + private: + const reader::EntrySchema& entry_schema_; + rapidjson::Value current_line_; + std::vector buffer_; + // get allocator from document + rapidjson::Document document_; +}; + +class ArrowJsonArrayExportWriter : public QueryExportWriter { + public: + ArrowJsonArrayExportWriter( + const reader::FileSchema& schema, + std::shared_ptr fileSystem, + std::shared_ptr entry_schema = nullptr) + : QueryExportWriter(schema, fileSystem, std::move(entry_schema)) {} + ~ArrowJsonArrayExportWriter() override = default; + + neug::Status writeTable(const QueryResponse* table) override; +}; + +class ArrowJsonLExportWriter : public QueryExportWriter { + public: + ArrowJsonLExportWriter( + const reader::FileSchema& schema, + std::shared_ptr fileSystem, + std::shared_ptr entry_schema = nullptr) + : QueryExportWriter(schema, fileSystem, std::move(entry_schema)) {} + ~ArrowJsonLExportWriter() override = default; + + neug::Status writeTable(const QueryResponse* table) override; +}; +} // namespace writer + +namespace function { +struct ExportJsonFunction : public ExportFunction { + static constexpr const char* name = "COPY_JSON"; + + static function_set getFunctionSet(); +}; + +struct ExportJsonLFunction : public ExportFunction { + static constexpr const char* name = "COPY_JSONL"; + + static function_set getFunctionSet(); +}; +} // namespace function +} // namespace neug diff --git a/extension/json/include/json_options.h b/extension/json/include/json_options.h index 5416bbb2..612406b5 100644 --- a/extension/json/include/json_options.h +++ b/extension/json/include/json_options.h @@ -28,8 +28,6 @@ namespace neug { namespace reader { struct JsonParseOptions { - Option newline_delimited = - Option::BoolOption("newline_delimited", false); Option newlines_in_values = Option::BoolOption("newlines_in_values", false); }; diff --git a/extension/json/include/json_read_function.h b/extension/json/include/json_read_function.h index 48537d7b..56baa9d4 100644 --- a/extension/json/include/json_read_function.h +++ b/extension/json/include/json_read_function.h @@ -40,14 +40,14 @@ struct JsonReadFunction { auto typeIDs = std::vector{common::LogicalTypeID::STRING}; auto readFunction = std::make_unique(name, typeIDs); - readFunction->execFunc = execFunc; - readFunction->sniffFunc = sniffFunc; + readFunction->execFunc = jsonExecFunc; + readFunction->sniffFunc = jsonSniffFunc; function_set functionSet; functionSet.push_back(std::move(readFunction)); return functionSet; } - static execution::Context execFunc( + static execution::Context jsonExecFunc( std::shared_ptr state) { // todo: get file system from vfs manager LocalFileSystemProvider fsProvider; @@ -55,6 +55,7 @@ struct JsonReadFunction { state->schema.file.paths = fileInfo.resolvedPaths; auto optionsBuilder = std::make_unique(state); + // register JsonDatasetBuilder to the reader to support json array format auto reader = std::make_unique( state, std::move(optionsBuilder), fileInfo.fileSystem, std::make_shared()); @@ -64,7 +65,7 @@ struct JsonReadFunction { return ctx; } - static std::shared_ptr sniffFunc( + static std::shared_ptr jsonSniffFunc( const reader::FileSchema& schema) { auto state = std::make_shared(); auto& externalSchema = state->schema; @@ -78,6 +79,7 @@ struct JsonReadFunction { state->schema.file.paths = fileInfo.resolvedPaths; auto optionsBuilder = std::make_unique(state); + // register JsonDatasetBuilder to the reader to support json array format auto reader = std::make_shared( state, std::move(optionsBuilder), fileInfo.fileSystem, std::make_shared()); @@ -92,9 +94,63 @@ struct JsonReadFunction { }; struct JsonLReadFunction { - using alias = JsonReadFunction; - static constexpr const char* name = "JSONL_SCAN"; + + static function_set getFunctionSet() { + auto typeIDs = + std::vector{common::LogicalTypeID::STRING}; + auto readFunction = std::make_unique(name, typeIDs); + readFunction->execFunc = jsonLExecFunc; + readFunction->sniffFunc = jsonLSniffFunc; + function_set functionSet; + functionSet.push_back(std::move(readFunction)); + return functionSet; + } + + static execution::Context jsonLExecFunc( + std::shared_ptr state) { + // todo: get file system from vfs manager + LocalFileSystemProvider fsProvider; + auto fileInfo = fsProvider.provide(state->schema.file); + state->schema.file.paths = fileInfo.resolvedPaths; + auto optionsBuilder = + std::make_unique(state); + // Arrow can support jsonl format by default, no need to register other + // DatasetBuilder + auto reader = std::make_unique( + state, std::move(optionsBuilder), fileInfo.fileSystem); + execution::Context ctx; + auto localState = std::make_shared(); + reader->read(localState, ctx); + return ctx; + } + + static std::shared_ptr jsonLSniffFunc( + const reader::FileSchema& schema) { + auto state = std::make_shared(); + auto& externalSchema = state->schema; + // create table entry schema with empty column names and types, which need + // to be inferred. + externalSchema.entry = std::make_shared(); + externalSchema.file = schema; + // todo: get file system from vfs manager + LocalFileSystemProvider fsProvider; + auto fileInfo = fsProvider.provide(state->schema.file); + state->schema.file.paths = fileInfo.resolvedPaths; + auto optionsBuilder = + std::make_unique(state); + // Arrow can support jsonl format by default, no need to register other + // DatasetBuilder + auto reader = std::make_shared( + state, std::move(optionsBuilder), fileInfo.fileSystem); + auto sniffer = std::make_shared(reader); + auto sniffResult = sniffer->sniff(); + if (!sniffResult) { + THROW_IO_EXCEPTION("Failed to sniff schema: " + + sniffResult.error().ToString()); + } + return sniffResult.value(); + } }; } // namespace function } // namespace neug \ No newline at end of file diff --git a/extension/json/src/json_dataset_builder.cc b/extension/json/src/json_dataset_builder.cc index 02a6eed8..d0424079 100644 --- a/extension/json/src/json_dataset_builder.cc +++ b/extension/json/src/json_dataset_builder.cc @@ -37,38 +37,6 @@ namespace neug { namespace reader { -std::shared_ptr DatasetBuilder::buildFactory( - std::shared_ptr sharedState, - std::shared_ptr fs, - std::shared_ptr fileFormat) { - if (!sharedState) { - THROW_INVALID_ARGUMENT_EXCEPTION("SharedState is null"); - } - if (!fs) { - THROW_INVALID_ARGUMENT_EXCEPTION("FileSystem is null"); - } - if (!fileFormat) { - THROW_INVALID_ARGUMENT_EXCEPTION("File format is null"); - } - - const auto& fileSchema = sharedState->schema.file; - const std::vector& file_paths = fileSchema.paths; - - if (file_paths.empty()) { - THROW_INVALID_ARGUMENT_EXCEPTION("No file paths provided"); - } - - arrow::dataset::FileSystemFactoryOptions factory_options; - factory_options.exclude_invalid_files = false; - auto factory_result = arrow::dataset::FileSystemDatasetFactory::Make( - fs, file_paths, fileFormat, factory_options); - if (!factory_result.ok()) { - THROW_IO_EXCEPTION("Failed to create FileSystemDatasetFactory: " + - factory_result.status().message()); - } - return factory_result.ValueOrDie(); -} - /** * @brief DatasetFactory for JSON file format * @@ -275,12 +243,6 @@ JsonDatasetBuilder::buildFactory( if (!sharedState) { THROW_INVALID_ARGUMENT_EXCEPTION("SharedState is null"); } - auto& file = sharedState->schema.file; - JsonParseOptions jsonOpts; - // if json format is newline_delimited, use the default buildFactory - if (jsonOpts.newline_delimited.get(file.options)) { - return DatasetBuilder::buildFactory(sharedState, fs, fileFormat); - } // For JSON_ARRAY format, use custom JsonDatasetFactory return std::make_shared(sharedState, fs, fileFormat); } diff --git a/extension/json/src/json_export_function.cc b/extension/json/src/json_export_function.cc new file mode 100644 index 00000000..e3d6a883 --- /dev/null +++ b/extension/json/src/json_export_function.cc @@ -0,0 +1,487 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "json_export_function.h" + +#include +#include +#include +#include +#include + +#include + +#include "neug/compiler/function/read_function.h" +#include "neug/generated/proto/response/response.pb.h" +#include "neug/utils/exception/exception.h" +#include "neug/utils/property/types.h" +#include "neug/utils/result.h" +#include "neug/utils/writer/writer.h" + +namespace neug { +namespace writer { + +#define TYPED_PRIMITIVE_ARRAY_TO_JSON_VALUE(CASE_ENUM, GETTER_METHOD, TYPE) \ + case neug::Array::TypedArrayCase::CASE_ENUM: { \ + auto& typed_array = arr.GETTER_METHOD(); \ + if (!StringFormatBuffer::validateProtoValue(typed_array.validity(), \ + rowIdx)) { \ + RETURN_STATUS_ERROR( \ + neug::StatusCode::ERR_INVALID_ARGUMENT, \ + "Value is invalid, rowIdx=" + std::to_string(rowIdx)); \ + } \ + rapidjson::Value v(static_cast(typed_array.values(rowIdx))); \ + return v; \ + } + +static neug::result parseJsonStringToValue( + const std::string& json_str, int rowIdx, rapidjson::Document& parse_doc, + const char* type_name) { + if (json_str.empty()) { + RETURN_STATUS_ERROR(neug::StatusCode::ERR_INVALID_ARGUMENT, + "Empty JSON string for " + std::string(type_name) + + " at row " + std::to_string(rowIdx)); + } + rapidjson::Document temp_doc(&parse_doc.GetAllocator()); + temp_doc.Parse(json_str.data(), json_str.size()); + if (temp_doc.HasParseError()) { + RETURN_STATUS_ERROR( + neug::StatusCode::ERR_INVALID_ARGUMENT, + "Invalid JSON for " + std::string(type_name) + " at row " + + std::to_string(rowIdx) + ": " + + rapidjson::GetParseError_En(temp_doc.GetParseError()) + + " at offset " + std::to_string(temp_doc.GetErrorOffset())); + } + rapidjson::Value v; + // use swap to avoid memory allocation + v.Swap(temp_doc); + return v; +} + +// return `rapidjson::Value` directly will not lead to any memory allocation, +// it's a move operation +static neug::result formatValueToJson( + const neug::Array& arr, int rowIdx, rapidjson::Document& doc) { + auto& allocator = doc.GetAllocator(); + switch (arr.typed_array_case()) { + TYPED_PRIMITIVE_ARRAY_TO_JSON_VALUE(kBoolArray, bool_array, bool) + TYPED_PRIMITIVE_ARRAY_TO_JSON_VALUE(kInt32Array, int32_array, int32_t) + TYPED_PRIMITIVE_ARRAY_TO_JSON_VALUE(kInt64Array, int64_array, int64_t) + TYPED_PRIMITIVE_ARRAY_TO_JSON_VALUE(kUint32Array, uint32_array, uint32_t) + TYPED_PRIMITIVE_ARRAY_TO_JSON_VALUE(kUint64Array, uint64_array, uint64_t) + TYPED_PRIMITIVE_ARRAY_TO_JSON_VALUE(kFloatArray, float_array, float) + TYPED_PRIMITIVE_ARRAY_TO_JSON_VALUE(kDoubleArray, double_array, double) + case neug::Array::TypedArrayCase::kStringArray: { + auto& string_array = arr.string_array(); + if (!StringFormatBuffer::validateProtoValue(string_array.validity(), + rowIdx)) { + RETURN_STATUS_ERROR(neug::StatusCode::ERR_INVALID_ARGUMENT, + "Value is invalid, rowIdx=" + std::to_string(rowIdx)); + } + const auto& str = string_array.values(rowIdx); + rapidjson::Value v; + v.SetString(str.c_str(), static_cast(str.size()), + allocator); + return v; + } + case neug::Array::TypedArrayCase::kDateArray: { + auto& date32_arr = arr.date_array(); + if (!StringFormatBuffer::validateProtoValue(date32_arr.validity(), + rowIdx)) { + RETURN_STATUS_ERROR(neug::StatusCode::ERR_INVALID_ARGUMENT, + "Value is invalid, rowIdx=" + std::to_string(rowIdx)); + } + Date date_value; + date_value.from_timestamp(date32_arr.values(rowIdx)); + const auto& s = date_value.to_string(); + rapidjson::Value v; + v.SetString(s.c_str(), static_cast(s.size()), + allocator); + return v; + } + case neug::Array::TypedArrayCase::kTimestampArray: { + auto& timestamp_array = arr.timestamp_array(); + if (!StringFormatBuffer::validateProtoValue(timestamp_array.validity(), + rowIdx)) { + RETURN_STATUS_ERROR(neug::StatusCode::ERR_INVALID_ARGUMENT, + "Value is invalid, rowIdx=" + std::to_string(rowIdx)); + } + DateTime dt_value(timestamp_array.values(rowIdx)); + const auto& s = dt_value.to_string(); + rapidjson::Value v; + v.SetString(s.c_str(), static_cast(s.size()), + allocator); + return v; + } + case neug::Array::TypedArrayCase::kIntervalArray: { + auto& interval_array = arr.interval_array(); + if (!StringFormatBuffer::validateProtoValue(interval_array.validity(), + rowIdx)) { + RETURN_STATUS_ERROR(neug::StatusCode::ERR_INVALID_ARGUMENT, + "Value is invalid, rowIdx=" + std::to_string(rowIdx)); + } + const auto& s = interval_array.values(rowIdx); + rapidjson::Value v; + v.SetString(s.c_str(), static_cast(s.size()), + allocator); + return v; + } + case neug::Array::TypedArrayCase::kListArray: { + auto& list_array = arr.list_array(); + if (!StringFormatBuffer::validateProtoValue(list_array.validity(), + rowIdx)) { + RETURN_STATUS_ERROR(neug::StatusCode::ERR_INVALID_ARGUMENT, + "Value is invalid, rowIdx=" + std::to_string(rowIdx)); + } + rapidjson::Value arr_val(rapidjson::kArrayType); + uint32_t list_size = + list_array.offsets(rowIdx + 1) - list_array.offsets(rowIdx); + size_t offset = list_array.offsets(rowIdx); + for (uint32_t i = 0; i < list_size; ++i) { + rapidjson::Value elem; + GS_ASSIGN(elem, formatValueToJson(list_array.elements(), + static_cast(offset + i), doc)); + arr_val.PushBack(std::move(elem), allocator); + } + return arr_val; + } + case neug::Array::TypedArrayCase::kStructArray: { + auto& struct_arr = arr.struct_array(); + if (!StringFormatBuffer::validateProtoValue(struct_arr.validity(), + rowIdx)) { + RETURN_STATUS_ERROR(neug::StatusCode::ERR_INVALID_ARGUMENT, + "Value is invalid, rowIdx=" + std::to_string(rowIdx)); + } + rapidjson::Value arr_val(rapidjson::kArrayType); + for (int i = 0; i < struct_arr.fields_size(); ++i) { + const auto& field = struct_arr.fields(i); + rapidjson::Value elem; + GS_ASSIGN(elem, formatValueToJson(field, rowIdx, doc)); + arr_val.PushBack(std::move(elem), allocator); + } + return arr_val; + } + case neug::Array::TypedArrayCase::kVertexArray: { + auto& vertex_array = arr.vertex_array(); + if (!StringFormatBuffer::validateProtoValue(vertex_array.validity(), + rowIdx)) { + RETURN_STATUS_ERROR(neug::StatusCode::ERR_INVALID_ARGUMENT, + "Value is invalid, rowIdx=" + std::to_string(rowIdx)); + } + return parseJsonStringToValue(vertex_array.values(rowIdx), rowIdx, doc, + "vertex"); + } + case neug::Array::TypedArrayCase::kEdgeArray: { + auto& edge_array = arr.edge_array(); + if (!StringFormatBuffer::validateProtoValue(edge_array.validity(), + rowIdx)) { + RETURN_STATUS_ERROR(neug::StatusCode::ERR_INVALID_ARGUMENT, + "Value is invalid, rowIdx=" + std::to_string(rowIdx)); + } + return parseJsonStringToValue(edge_array.values(rowIdx), rowIdx, doc, + "edge"); + } + case neug::Array::TypedArrayCase::kPathArray: { + auto& path_array = arr.path_array(); + if (!StringFormatBuffer::validateProtoValue(path_array.validity(), + rowIdx)) { + RETURN_STATUS_ERROR(neug::StatusCode::ERR_INVALID_ARGUMENT, + "Value is invalid, rowIdx=" + std::to_string(rowIdx)); + } + return parseJsonStringToValue(path_array.values(rowIdx), rowIdx, doc, + "path"); + } + default: + RETURN_STATUS_ERROR( + neug::StatusCode::ERR_INVALID_ARGUMENT, + "Unsupported type: " + std::to_string(arr.typed_array_case())); + } +} + +static std::string getColumnName(const reader::EntrySchema& entry_schema, + size_t colIdx) { + if (colIdx < entry_schema.columnNames.size()) { + return entry_schema.columnNames[colIdx]; + } + LOG(WARNING) << "Column index out of range: colIdx=" << colIdx + << ", using default column name"; + return "col_" + std::to_string(colIdx); +} + +JsonArrayStringFormatBuffer::JsonArrayStringFormatBuffer( + const neug::QueryResponse* response, const reader::FileSchema& schema, + const reader::EntrySchema& entry_schema) + : StringFormatBuffer(response, schema), entry_schema_(entry_schema) { + buffer_.SetArray(); + current_line_.SetObject(); +} + +void JsonArrayStringFormatBuffer::addValue(int rowIdx, int colIdx) { + if (!validateIndex(response_, rowIdx, colIdx)) { + THROW_IO_EXCEPTION( + "Value index out of range: rowIdx=" + std::to_string(rowIdx) + + ", colIdx=" + std::to_string(colIdx)); + } + const neug::Array& column = response_->arrays(colIdx); + auto jsonResult = formatValueToJson(column, rowIdx, document_); + auto& allocator = document_.GetAllocator(); + WriteOptions writeOpts; + bool ignoreErrors = writeOpts.ignore_errors.get(schema_.options); + if (!jsonResult && !ignoreErrors) { + THROW_IO_EXCEPTION( + "Format value to JSON failed, rowIdx=" + std::to_string(rowIdx) + + ", colIdx=" + std::to_string(colIdx) + + ", error=" + jsonResult.error().ToString()); + } + const auto& columnName = getColumnName(entry_schema_, colIdx); + rapidjson::Value key(columnName.c_str(), + static_cast(columnName.size()), + allocator); + if (jsonResult) { + current_line_.AddMember(key, std::move(*jsonResult), allocator); + } else { + // add null value to ignore errors + current_line_.AddMember(key, rapidjson::Value(rapidjson::kNullType), + allocator); + } + if (colIdx == static_cast(response_->arrays_size()) - 1) { + buffer_.PushBack(std::move(current_line_), allocator); + current_line_.SetObject(); + } +} + +neug::Status JsonArrayStringFormatBuffer::flush( + std::shared_ptr stream) { + if (buffer_.IsArray() && buffer_.Empty()) { + return neug::Status::OK(); + } + const auto& jsonStr = rapidjson_stringify(buffer_); + buffer_.Clear(); + auto writer_res = stream->Write(jsonStr.c_str(), jsonStr.size()); + if (writer_res.ok()) { + return neug::Status::OK(); + } + return neug::Status( + neug::StatusCode::ERR_IO_ERROR, + "Failed to write JSON to stream: " + writer_res.ToString()); +} + +JsonLStringFormatBuffer::JsonLStringFormatBuffer( + const neug::QueryResponse* response, const reader::FileSchema& schema, + const reader::EntrySchema& entry_schema) + : StringFormatBuffer(response, schema), entry_schema_(entry_schema) { + current_line_.SetObject(); + WriteOptions writeOpts; + size_t batchSize = writeOpts.batch_rows.get(schema.options); + if (batchSize > 0 && response->row_count() > 0) { + buffer_.reserve(batchSize); + } +} + +void JsonLStringFormatBuffer::addValue(int rowIdx, int colIdx) { + if (!validateIndex(response_, rowIdx, colIdx)) { + THROW_IO_EXCEPTION( + "Value index out of range: rowIdx=" + std::to_string(rowIdx) + + ", colIdx=" + std::to_string(colIdx)); + } + const neug::Array& column = response_->arrays(colIdx); + auto jsonResult = formatValueToJson(column, rowIdx, document_); + auto& allocator = document_.GetAllocator(); + WriteOptions writeOpts; + bool ignoreErrors = writeOpts.ignore_errors.get(schema_.options); + if (!jsonResult && !ignoreErrors) { + THROW_IO_EXCEPTION( + "Format value to JSON failed, rowIdx=" + std::to_string(rowIdx) + + ", colIdx=" + std::to_string(colIdx) + + ", error=" + jsonResult.error().ToString()); + } + const auto& columnName = getColumnName(entry_schema_, colIdx); + rapidjson::Value key(columnName.c_str(), + static_cast(columnName.size()), + allocator); + if (jsonResult) { + current_line_.AddMember(key, std::move(*jsonResult), allocator); + } else { + current_line_.AddMember(key, rapidjson::Value(rapidjson::kNullType), + allocator); + } + if (colIdx == static_cast(response_->arrays_size()) - 1) { + buffer_.push_back(std::move(current_line_)); + current_line_.SetObject(); + } +} + +neug::Status JsonLStringFormatBuffer::flush( + std::shared_ptr stream) { + for (const auto& val : buffer_) { + const auto& jsonStr = rapidjson_stringify(val); + auto ar_status = stream->Write(jsonStr.c_str(), jsonStr.size()); + if (!ar_status.ok()) { + return neug::Status(neug::StatusCode::ERR_IO_ERROR, + "Failed to write JSON line: " + ar_status.ToString()); + } + ar_status = stream->Write(DEFAULT_JSON_NEWLINE, sizeof(char)); + if (!ar_status.ok()) { + return neug::Status(neug::StatusCode::ERR_IO_ERROR, + "Failed to write newline: " + ar_status.ToString()); + } + } + buffer_.clear(); + return neug::Status::OK(); +} + +static Status writeTableWithBuffer( + StringFormatBuffer& buffer, const reader::FileSchema& schema, + const std::shared_ptr& fileSystem, + const neug::QueryResponse* table, size_t batchSize) { + if (schema.paths.empty()) { + return Status(StatusCode::ERR_INVALID_ARGUMENT, "Schema paths is empty"); + } + auto stream_result = fileSystem->OpenOutputStream(schema.paths[0]); + if (!stream_result.ok()) { + return Status( + StatusCode::ERR_IO_ERROR, + "Failed to open file stream: " + stream_result.status().ToString()); + } + auto stream = stream_result.ValueOrDie(); + + if (batchSize == 0) { + return Status(StatusCode::ERR_INVALID_ARGUMENT, + "Batch size should be positive"); + } + + for (size_t i = 0; i < table->row_count(); ++i) { + for (size_t j = 0; j < table->arrays_size(); ++j) { + buffer.addValue(static_cast(i), static_cast(j)); + } + if ((i + 1) % static_cast(batchSize) == 0) { + auto status = buffer.flush(stream); + if (!status.ok()) { + (void) stream->Close(); + return Status(StatusCode::ERR_IO_ERROR, + "Failed to flush JSON buffer: " + status.ToString()); + } + } + } + + auto status = buffer.flush(stream); + if (!status.ok()) { + (void) stream->Close(); + return Status(StatusCode::ERR_IO_ERROR, + "Failed to flush JSON buffer: " + status.ToString()); + } + auto close_status = stream->Close(); + if (!close_status.ok()) { + return Status(StatusCode::ERR_IO_ERROR, + "Failed to close output stream: " + close_status.ToString()); + } + return Status::OK(); +} + +Status ArrowJsonArrayExportWriter::writeTable( + const neug::QueryResponse* table) { + if (!entry_schema_) { + return Status(StatusCode::ERR_INVALID_ARGUMENT, "entry_schema is null"); + } + JsonArrayStringFormatBuffer buffer(table, schema_, *entry_schema_); + size_t batchSize = table->row_count(); + if (batchSize == 0) { + batchSize = 1; + } + // JSON Array is one single array; only flush once at the end. + return writeTableWithBuffer(buffer, schema_, fileSystem_, table, batchSize); +} + +Status ArrowJsonLExportWriter::writeTable(const neug::QueryResponse* table) { + if (!entry_schema_) { + return Status(StatusCode::ERR_INVALID_ARGUMENT, "entry_schema is null"); + } + JsonLStringFormatBuffer buffer(table, schema_, *entry_schema_); + WriteOptions writeOpts; + size_t batchSize = writeOpts.batch_rows.get(schema_.options); + // JSONL: each line is a separate JSON object; safe to flush per batch. + return writeTableWithBuffer(buffer, schema_, fileSystem_, table, batchSize); +} +} // namespace writer + +namespace function { +// write json in array format +static execution::Context jsonExecFunc( + neug::execution::Context& ctx, reader::FileSchema& schema, + const std::shared_ptr& entry_schema, + const neug::StorageReadInterface& graph) { + if (schema.paths.empty()) { + THROW_INVALID_ARGUMENT_EXCEPTION("Schema paths is empty"); + } + LocalFileSystemProvider fsProvider; + auto fileInfo = fsProvider.provide(schema, false); + auto writer = std::make_shared( + schema, fileInfo.fileSystem, entry_schema); + auto status = writer->write(ctx, graph); + if (!status.ok()) { + THROW_IO_EXCEPTION("Export failed: " + status.ToString()); + } + ctx.clear(); + return ctx; +} + +static std::unique_ptr bindFunc( + ExportFuncBindInput& bindInput) { + return std::make_unique( + bindInput.columnNames, bindInput.filePath, bindInput.parsingOptions); +} + +function_set ExportJsonFunction::getFunctionSet() { + function_set functionSet; + auto exportFunc = std::make_unique(name); + exportFunc->bind = bindFunc; + exportFunc->execFunc = jsonExecFunc; + functionSet.push_back(std::move(exportFunc)); + return functionSet; +} +} // namespace function + +namespace function { +// write json in newline-delimited format +static execution::Context jsonLExecFunc( + neug::execution::Context& ctx, reader::FileSchema& schema, + const std::shared_ptr& entry_schema, + const neug::StorageReadInterface& graph) { + if (schema.paths.empty()) { + THROW_INVALID_ARGUMENT_EXCEPTION("Schema paths is empty"); + } + LocalFileSystemProvider fsProvider; + auto fileInfo = fsProvider.provide(schema, false); + auto writer = std::make_shared( + schema, fileInfo.fileSystem, entry_schema); + auto status = writer->write(ctx, graph); + if (!status.ok()) { + THROW_IO_EXCEPTION("Export failed: " + status.ToString()); + } + ctx.clear(); + return ctx; +} + +function_set ExportJsonLFunction::getFunctionSet() { + function_set functionSet; + auto exportFunc = std::make_unique(name); + exportFunc->bind = bindFunc; + exportFunc->execFunc = jsonLExecFunc; + functionSet.push_back(std::move(exportFunc)); + return functionSet; +} +} // namespace function +} // namespace neug diff --git a/extension/json/src/json_extension.cpp b/extension/json/src/json_extension.cpp index 1f28f5fd..50889bd2 100644 --- a/extension/json/src/json_extension.cpp +++ b/extension/json/src/json_extension.cpp @@ -13,6 +13,7 @@ * limitations under the License. */ +#include "json_export_function.h" #include "neug/compiler/extension/extension_api.h" #include "neug/utils/exception/exception.h" @@ -27,10 +28,18 @@ void Init() { neug::function::JsonReadFunction>( neug::catalog::CatalogEntryType::TABLE_FUNCTION_ENTRY); - neug::extension::ExtensionAPI::registerFunctionAlias< + neug::extension::ExtensionAPI::registerFunction< neug::function::JsonLReadFunction>( neug::catalog::CatalogEntryType::TABLE_FUNCTION_ENTRY); + // Register JSON export functions + neug::extension::ExtensionAPI::registerFunction< + neug::function::ExportJsonFunction>( + neug::catalog::CatalogEntryType::COPY_FUNCTION_ENTRY); + neug::extension::ExtensionAPI::registerFunction< + neug::function::ExportJsonLFunction>( + neug::catalog::CatalogEntryType::COPY_FUNCTION_ENTRY); + neug::extension::ExtensionAPI::registerExtension( neug::extension::ExtensionInfo{ "json", "Provides functions to read and write JSON files."}); diff --git a/include/neug/utils/reader/schema.h b/include/neug/utils/reader/schema.h index 4623a293..b52e5d75 100644 --- a/include/neug/utils/reader/schema.h +++ b/include/neug/utils/reader/schema.h @@ -39,6 +39,7 @@ struct EntrySchema { virtual ~EntrySchema() = default; virtual EntrySchemaType type() const = 0; std::vector columnNames; + // todo: support vertex, edge and path types std::vector> columnTypes; template diff --git a/include/neug/utils/writer/writer.h b/include/neug/utils/writer/writer.h index 54fe418b..e7acac03 100644 --- a/include/neug/utils/writer/writer.h +++ b/include/neug/utils/writer/writer.h @@ -69,15 +69,13 @@ class StringFormatBuffer { virtual void addValue(int rowIdx, int colIdx) = 0; virtual neug::Status flush( std::shared_ptr stream) = 0; + static bool validateIndex(const neug::QueryResponse* response, int rowIdx, + int colIdx); + static bool validateProtoValue(const std::string& validity, int rowIdx); protected: const neug::QueryResponse* response_; const reader::FileSchema& schema_; - - protected: - bool validateIndex(const neug::QueryResponse* response, int rowIdx, - int colIdx); - bool validateProtoValue(const std::string& validity, int rowIdx); }; struct BinaryData { diff --git a/tests/unittest/test_extension.cc b/tests/unittest/test_extension.cc index 29e08b63..7515fdd6 100644 --- a/tests/unittest/test_extension.cc +++ b/tests/unittest/test_extension.cc @@ -124,7 +124,7 @@ TEST_F(TestJsonExtension, VPersonJsonl) { << "LOAD json failed: " << load_res.error().ToString(); std::string import_query = "COPY person FROM (LOAD FROM \"" + vperson_jsonl + - "\" (newline_delimited=true) RETURN ID, fName, " + "\" RETURN ID, fName, " "gender, age, eyesight, height);"; auto import_res = conn->Query(import_query); ASSERT_TRUE(import_res.has_value()) diff --git a/tools/python_bind/example/complex_test.py b/tools/python_bind/example/complex_test.py index 3af0a119..fbcb9e86 100644 --- a/tools/python_bind/example/complex_test.py +++ b/tools/python_bind/example/complex_test.py @@ -25,6 +25,7 @@ - tutorials : tinysnb builtin dataset exploration """ +import json import os import shutil import sys @@ -110,7 +111,7 @@ def verify_json_extension_loaded(conn_json): fail("SHOW_LOADED_EXTENSIONS", e) -def run_json_array_tests(conn_json): +def run_json_array_tests(conn_json, export_dir=None): if not os.path.isfile(JSON_ARRAY_FILE): fail(f"JSON Array file not found: {JSON_ARRAY_FILE}") return @@ -155,8 +156,27 @@ def _alias(rows): _alias, ) + # Export test: COPY LOAD result to JSON array file and verify + if export_dir: + export_path = os.path.join(export_dir, "export_array.json") + try: + conn_json.execute( + f'COPY (LOAD FROM "{JSON_ARRAY_FILE}" RETURN fName, age) TO ' + f"'{export_path}';" + ) + with open(export_path, encoding="utf-8") as f: + data = json.load(f) + assert isinstance(data, list), "Expected JSON array" + assert len(data) > 0, "Expected at least one exported row" + if data: + first = data[0] + assert isinstance(first, dict), "Each row should be a JSON object" + ok(f"Export to JSON array: {len(data)} rows written to {export_path}") + except Exception as e: + fail("Export LOAD result to JSON array", e) + -def run_jsonl_tests(conn_json): +def run_jsonl_tests(conn_json, export_dir=None): if not os.path.isfile(JSONL_FILE): fail(f"JSONL file not found: {JSONL_FILE}") return @@ -171,7 +191,7 @@ def _load_all(rows): run_query_with_handler( conn_json, "LOAD FROM JSONL file", - f'LOAD FROM "{JSONL_FILE}" (newline_delimited=true) RETURN *;', + f'LOAD FROM "{JSONL_FILE}" RETURN *;', _load_all, print_traceback=True, ) @@ -183,14 +203,32 @@ def _projection(rows): run_query_with_handler( conn_json, "JSONL column projection", - f'LOAD FROM "{JSONL_FILE}" (newline_delimited=true) RETURN fName, age;', + f'LOAD FROM "{JSONL_FILE}" RETURN fName, age;', _projection, ) + # Export test: COPY LOAD result to JSONL file and verify + if export_dir: + export_path = os.path.join(export_dir, "export_lines.jsonl") + try: + conn_json.execute( + f'COPY (LOAD FROM "{JSONL_FILE}" RETURN fName, age) TO ' + f"'{export_path}';" + ) + with open(export_path, encoding="utf-8") as f: + lines = [line.strip() for line in f if line.strip()] + data = [json.loads(line) for line in lines] + assert len(data) > 0, "Expected at least one exported line" + if data: + first = data[0] + assert isinstance(first, dict), "Each line should be a JSON object" + ok(f"Export to JSONL: {len(data)} lines written to {export_path}") + except Exception as e: + fail("Export LOAD result to JSONL", e) + def run_json_extension_suite(db_json, conn_json, db_path_json): statements = [ - ("INSTALL JSON succeeded", "INSTALL JSON;"), ("LOAD JSON succeeded", "LOAD JSON;"), ] @@ -198,8 +236,8 @@ def run_json_extension_suite(db_json, conn_json, db_path_json): run_statement(conn_json, desc, stmt) verify_json_extension_loaded(conn_json) - run_json_array_tests(conn_json) - run_jsonl_tests(conn_json) + run_json_array_tests(conn_json, export_dir=db_path_json) + run_jsonl_tests(conn_json, export_dir=db_path_json) conn_json.close() db_json.close() @@ -532,18 +570,24 @@ def _network_stats(): # ================================================================ section("5. Extensions — JSON Extension (Install / Load / Query)") -conn_json = None -db_path_json = tempfile.mkdtemp(prefix="neug_json_ext_") -try: - db_json = neug.Database(db_path_json) - conn_json = db_json.connect() - ok(f"Created persistent database for JSON extension test at {db_path_json}") -except Exception as e: - fail("Create database for JSON extension", e) - db_json = None +_run_ext_tests = os.environ.get("NEUG_RUN_EXTENSION_TESTS", "").strip().lower() +_run_ext_tests = _run_ext_tests in ("1", "true", "on", "yes") + +if not _run_ext_tests: + print(" (skipped: set NEUG_RUN_EXTENSION_TESTS=1 to run extension tests)") +else: + conn_json = None + db_path_json = tempfile.mkdtemp(prefix="neug_json_ext_") + try: + db_json = neug.Database(db_path_json) + conn_json = db_json.connect() + ok(f"Created persistent database for JSON extension test at {db_path_json}") + except Exception as e: + fail("Create database for JSON extension", e) + db_json = None -if db_json is not None and conn_json is not None: - run_json_extension_suite(db_json, conn_json, db_path_json) + if db_json is not None and conn_json is not None: + run_json_extension_suite(db_json, conn_json, db_path_json) # ================================================================ # Summary diff --git a/tools/python_bind/pyproject.toml b/tools/python_bind/pyproject.toml index da8260be..2d486a35 100644 --- a/tools/python_bind/pyproject.toml +++ b/tools/python_bind/pyproject.toml @@ -63,7 +63,7 @@ test = [ [tool.cibuildwheel] build = "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-*" skip = ["*-musllinux_*", "*i686*"] -environment-pass = ["BUILD_TYPE", "CMAKE_BUILD_PARALLEL_LEVEL", "CMAKE_PREFIX_PATH", "MACOSX_DEPLOYMENT_TARGET", "PYBIND11_FINDPYTHON"] +environment-pass = ["BUILD_TYPE", "CMAKE_BUILD_PARALLEL_LEVEL", "CMAKE_PREFIX_PATH", "MACOSX_DEPLOYMENT_TARGET", "PYBIND11_FINDPYTHON", "CI_INSTALL_EXTENSIONS"] manylinux-x86_64-image = "neug-registry.cn-hongkong.cr.aliyuncs.com/neug/neug-manylinux:v0.1.0-x86_64" manylinux-aarch64-image = "neug-registry.cn-hongkong.cr.aliyuncs.com/neug/neug-manylinux:v0.1.0-arm64" diff --git a/tools/python_bind/setup.py b/tools/python_bind/setup.py index f2c91264..8d2b8dc0 100644 --- a/tools/python_bind/setup.py +++ b/tools/python_bind/setup.py @@ -33,6 +33,7 @@ from setuptools import setup from setuptools.command.build_ext import build_ext from setuptools.command.build_py import build_py as _build_py +from setuptools.command.install_lib import install_lib as _install_lib if sys.version_info >= (3, 12): from setuptools import Command # noqa: F811 @@ -152,11 +153,13 @@ def build_extension(self, ext: CMakeExtension) -> None: # noqa: C901 f"-DBUILD_HTTP_SERVER={build_http_server}", f"-DWITH_MIMALLOC={with_mimalloc}", f"-DENABLE_GCOV={enable_gcov}", - f"-DBUILD_EXTENSIONS={build_extensions}", "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ] if build_extensions: cmake_args.append(f"-DBUILD_EXTENSIONS={build_extensions}") + install_extensions = os.environ.get("CI_INSTALL_EXTENSIONS", "") + if install_extensions: + cmake_args.append(f"-DBUILD_EXTENSIONS={install_extensions}") if cmake_install_prefix: cmake_args += [ f"-DCMAKE_INSTALL_PREFIX={cmake_install_prefix}", @@ -327,6 +330,57 @@ def run(self): return super().run() +class InstallLib(_install_lib): + """Ensure extension/* (e.g. extension/json/libjson.neug_extension) is copied. + + CMake writes native extensions to build_lib/extension//. + Only runs when CI_INSTALL_EXTENSIONS is set (semicolon-separated, e.g. json;parquet). + Copies only the listed extensions so the wheel gets site-packages/extension/... + """ + + def run(self): + super().run() + # Only copy extensions when INSTALL_EXTENSIONS is set (e.g. json;parquet) + install_extensions = os.environ.get("CI_INSTALL_EXTENSIONS", "").strip() + print( + f"[InstallLib] INSTALL_EXTENSIONS={repr(install_extensions)} " + f"build_dir={self.build_dir!r} install_dir={self.install_dir!r}" + ) + sys.stdout.flush() + if not install_extensions: + print("[InstallLib] Skip extension copy (INSTALL_EXTENSIONS empty)") + sys.stdout.flush() + return + names = [n.strip() for n in install_extensions.split(";") if n.strip()] + if not names: + print("[InstallLib] Skip extension copy (no names after split)") + sys.stdout.flush() + return + ext_src_base = os.path.join(self.build_dir, "extension") + ext_dst_base = os.path.join(self.install_dir, "extension") + print( + f"[InstallLib] ext_src_base={ext_src_base!r} exists={os.path.isdir(ext_src_base)}" + ) + sys.stdout.flush() + if not os.path.isdir(ext_src_base): + print("[InstallLib] Skip (extension source dir missing)") + sys.stdout.flush() + return + for name in names: + src = os.path.join(ext_src_base, name) + if not os.path.isdir(src): + continue + dst = os.path.join(ext_dst_base, name) + os.makedirs(dst, exist_ok=True) + for f in os.listdir(src): + s = os.path.join(src, f) + d = os.path.join(dst, f) + if os.path.isfile(s): + shutil.copy2(s, d) + print(f"[InstallLib] Copied extension: {name} -> {dst}") + sys.stdout.flush() + + setup( name="neug", version=version, @@ -338,7 +392,7 @@ def run(self): long_description=open(os.path.join(base_dir, "README.md"), "r").read(), long_description_content_type="text/markdown", packages=find_packages(exclude=["tests"]), - package_data={"neug": ["resources/*", "extension/*/*.neug_extension"]}, + package_data={"neug": ["resources/*"]}, zip_safe=False, include_package_data=True, entry_points={ @@ -361,5 +415,6 @@ def run(self): "build_py": BuildExtFirst, "build_ext": CMakeBuild, "build_proto": BuildProto, + "install_lib": InstallLib, }, ) diff --git a/tools/python_bind/tests/test_export.py b/tools/python_bind/tests/test_export.py index 4283578c..8cac2be8 100644 --- a/tools/python_bind/tests/test_export.py +++ b/tools/python_bind/tests/test_export.py @@ -17,6 +17,7 @@ # import csv +import json import os import shutil import sys @@ -27,6 +28,17 @@ from neug.database import Database +EXTENSION_TESTS_ENABLED = os.environ.get("NEUG_RUN_EXTENSION_TESTS", "").lower() in ( + "1", + "true", + "yes", + "on", +) +extension_test = pytest.mark.skipif( + not EXTENSION_TESTS_ENABLED, + reason="Extension tests disabled by default; set NEUG_RUN_EXTENSION_TESTS=1 to enable.", +) + def _count_query(conn, cypher): """Execute query and return number of result rows.""" @@ -45,6 +57,29 @@ def _parse_csv(path, delimiter="|", has_header=True): return (None, rows) +def _parse_json_array(path): + """Parse a JSON array file; returns list of objects. Empty file returns [].""" + with open(path, encoding="utf-8") as f: + text = f.read().strip() + if not text: + return [] + data = json.loads(text) + assert isinstance(data, list), f"Expected JSON array, got {type(data)}" + return data + + +def _parse_jsonl(path): + """Parse a JSONL file (one JSON object per line); returns list of objects.""" + rows = [] + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + rows.append(json.loads(line)) + return rows + + class TestExport: """COPY TO CSV tests using tinysnb. Assert header and data row count only.""" @@ -482,3 +517,169 @@ def test_export_with_escape_char(self): assert content == '"John\\"s"\n' finally: self.conn.execute("MATCH (v:person {ID: 1006}) DELETE v") + + @extension_test + def test_export_person_json_array(self): + """Export scalar columns to a single JSON array; verify row count and keys.""" + out_path = self.tmp_path / "person.json" + out_path.unlink(missing_ok=True) + expected = _count_query(self.conn, "MATCH (v:person) RETURN v.fName, v.age") + self.conn.execute("LOAD JSON") + self.conn.execute( + f"COPY (MATCH (v:person) RETURN v.fName, v.age) TO '{out_path}';" + ) + assert out_path.exists(), f"Output file not created: {out_path}" + data = _parse_json_array(out_path) + assert ( + len(data) == expected + ), f"Expected {expected} rows in JSON array, got {len(data)}" + if data: + first = data[0] + assert isinstance(first, dict), "Each row should be a JSON object" + assert ( + "fName" in first or "v.fName" in first + ), "First row should have fName (or v.fName) key" + assert ( + "age" in first or "v.age" in first + ), "First row should have age (or v.age) key" + + @extension_test + def test_export_person_node_json_array(self): + """Export full node to a single JSON array; verify row count and structure.""" + out_path = self.tmp_path / "person_node.json" + out_path.unlink(missing_ok=True) + expected = _count_query(self.conn, "MATCH (v:person) RETURN v") + self.conn.execute("LOAD JSON") + self.conn.execute(f"COPY (MATCH (v:person) RETURN v) TO '{out_path}';") + assert out_path.exists(), f"Output file not created: {out_path}" + data = _parse_json_array(out_path) + assert ( + len(data) == expected + ), f"Expected {expected} rows in JSON array, got {len(data)}" + if data: + first = data[0] + assert isinstance(first, dict), "Each row should be a JSON object" + + @extension_test + def test_export_person_jsonl(self): + """Export scalar columns to JSONL (one JSON object per line); verify count and keys.""" + out_path = self.tmp_path / "person.jsonl" + out_path.unlink(missing_ok=True) + expected = _count_query(self.conn, "MATCH (v:person) RETURN v.fName, v.age") + self.conn.execute("LOAD JSON") + self.conn.execute( + f"COPY (MATCH (v:person) RETURN v.fName, v.age) TO '{out_path}';" + ) + assert out_path.exists(), f"Output file not created: {out_path}" + rows = _parse_jsonl(out_path) + assert ( + len(rows) == expected + ), f"Expected {expected} lines in JSONL, got {len(rows)}" + if rows: + first = rows[0] + assert isinstance(first, dict), "Each line should be a JSON object" + assert ( + "fName" in first or "v.fName" in first + ), "First row should have fName (or v.fName) key" + assert ( + "age" in first or "v.age" in first + ), "First row should have age (or v.age) key" + + @extension_test + def test_export_person_node_jsonl(self): + """Export full node to JSONL (one JSON object per line); verify row count.""" + out_path = self.tmp_path / "person_node.jsonl" + out_path.unlink(missing_ok=True) + expected = _count_query(self.conn, "MATCH (v:person) RETURN v") + self.conn.execute("LOAD JSON") + self.conn.execute(f"COPY (MATCH (v:person) RETURN v) TO '{out_path}';") + assert out_path.exists(), f"Output file not created: {out_path}" + rows = _parse_jsonl(out_path) + assert ( + len(rows) == expected + ), f"Expected {expected} lines in JSONL, got {len(rows)}" + if rows: + assert isinstance(rows[0], dict), "Each line should be a JSON object" + + @extension_test + def test_export_collect_names_jsonl(self): + """Export collect names to JSONL (one JSON object per line); verify row count.""" + out_path = self.tmp_path / "collect_names.jsonl" + out_path.unlink(missing_ok=True) + expected = _count_query( + self.conn, "MATCH (v:person) RETURN v.ID, collect(v.fName)" + ) + self.conn.execute("LOAD JSON") + self.conn.execute( + f"COPY (MATCH (v:person) RETURN v.ID, collect(v.fName)) TO '{out_path}';" + ) + assert out_path.exists(), f"Output file not created: {out_path}" + rows = _parse_jsonl(out_path) + assert ( + len(rows) == expected + ), f"Expected {expected} lines in JSONL, got {len(rows)}" + if rows: + assert isinstance(rows[0], dict), "Each line should be a JSON object" + + +class TestExportComprehensiveGraph: + """COPY TO CSV/JSON tests using comprehensive_graph (bulk-loaded to /tmp/comprehensive_graph in CI).""" + + @pytest.fixture(autouse=True) + def setup(self, tmp_path): + self.db_dir = "/tmp/comprehensive_graph" + if not os.path.exists(self.db_dir): + pytest.fail(f"Database not found at {self.db_dir}") + self.db = Database(db_path=self.db_dir, mode="w") + self.conn = self.db.connect() + self.tmp_path = tmp_path + yield + self.conn.close() + self.db.close() + shutil.rmtree(self.tmp_path, ignore_errors=True) + + def test_export_comprehensive_graph_to_csv(self): + """Export node_a vertices from comprehensive_graph to CSV; verify header and row count.""" + out_path = self.tmp_path / "node_a.csv" + out_path.unlink(missing_ok=True) + expected = _count_query(self.conn, "MATCH (v:node_a) RETURN v.*") + self.conn.execute( + f"COPY (MATCH (v:node_a) RETURN v.*) TO " f"'{out_path}' (HEADER = true);" + ) + assert out_path.exists() + header, rows = _parse_csv(out_path, "|", has_header=True) + assert header is not None and len(header) == 11 + assert len(rows) == expected + + @extension_test + def test_export_comprehensive_graph_node_to_json_array(self): + """Export node_a vertices from comprehensive_graph to JSON array; verify row count and structure.""" + out_path = self.tmp_path / "node_a.json" + out_path.unlink(missing_ok=True) + expected = _count_query(self.conn, "MATCH (v:node_a) RETURN v.*") + self.conn.execute("LOAD JSON") + self.conn.execute(f"COPY (MATCH (v:node_a) RETURN v.*) TO '{out_path}';") + assert out_path.exists(), f"Output file not created: {out_path}" + data = _parse_json_array(out_path) + assert ( + len(data) == expected + ), f"Expected {expected} rows in JSON array, got {len(data)}" + if data: + first = data[0] + assert isinstance(first, dict), "Each row should be a JSON object" + + @extension_test + def test_export_comprehensive_graph_node_to_jsonl(self): + """Export node_a vertices from comprehensive_graph to JSONL; verify row count and structure.""" + out_path = self.tmp_path / "node_a.jsonl" + out_path.unlink(missing_ok=True) + expected = _count_query(self.conn, "MATCH (v:node_a) RETURN v.*") + self.conn.execute("LOAD JSON") + self.conn.execute(f"COPY (MATCH (v:node_a) RETURN v.*) TO '{out_path}';") + assert out_path.exists(), f"Output file not created: {out_path}" + rows = _parse_jsonl(out_path) + assert ( + len(rows) == expected + ), f"Expected {expected} lines in JSONL, got {len(rows)}" + if rows: + assert isinstance(rows[0], dict), "Each line should be a JSON object" diff --git a/tools/python_bind/tests/test_load.py b/tools/python_bind/tests/test_load.py index e1b0ff1a..9e6b89b4 100644 --- a/tools/python_bind/tests/test_load.py +++ b/tools/python_bind/tests/test_load.py @@ -26,15 +26,15 @@ from neug import Database -JSON_TESTS_ENABLED = os.environ.get("NEUG_RUN_JSON_TESTS", "").lower() in ( +EXTENSION_TESTS_ENABLED = os.environ.get("NEUG_RUN_EXTENSION_TESTS", "").lower() in ( "1", "true", "yes", "on", ) -json_test = pytest.mark.skipif( - not JSON_TESTS_ENABLED, - reason="JSON tests disabled by default; set NEUG_RUN_JSON_TESTS=1 to enable.", +extension_test = pytest.mark.skipif( + not EXTENSION_TESTS_ENABLED, + reason="Extension tests disabled by default; set NEUG_RUN_EXTENSION_TESTS=1 to enable.", ) @@ -771,7 +771,7 @@ def test_load_from_with_cast_and_where(self): assert isinstance(record[1], float), "age_double should be float" assert record[1] > 30.0, f"Age {record[1]} should be greater than 30.0" - @json_test + @extension_test def test_load_from_json_basic_return_all(self): """Test basic LOAD FROM JSON with RETURN *.""" json_path = os.path.join(self.tinysnb_path, "json", "vPerson.json") @@ -796,7 +796,7 @@ def test_load_from_json_basic_return_all(self): first_record = records[0] assert len(first_record) == 16, f"Expected 16 columns, got {len(first_record)}" - @json_test + @extension_test def test_load_from_json_return_specific_columns(self): """Test LOAD FROM JSON Array with column projection.""" json_path = os.path.join(self.tinysnb_path, "json", "vPerson.json") @@ -819,7 +819,7 @@ def test_load_from_json_return_specific_columns(self): assert isinstance(first_record[0], str), "fName should be string" assert isinstance(first_record[1], int), "age should be integer" - @json_test + @extension_test def test_load_from_json_with_column_alias(self): """Test LOAD FROM JSON Array with column aliases in RETURN. @@ -850,7 +850,7 @@ def test_load_from_json_with_column_alias(self): assert first_record[0] == "Alice", f"Expected 'Alice', got '{first_record[0]}'" assert first_record[1] == 35, f"Expected 35, got {first_record[1]}" - @json_test + @extension_test def test_load_from_jsonl_with_column_alias(self): """Test LOAD FROM JSONL with column aliases in RETURN.""" jsonl_path = os.path.join(self.tinysnb_path, "json", "vPerson.jsonl") @@ -860,7 +860,7 @@ def test_load_from_jsonl_with_column_alias(self): self.conn.execute("LOAD JSON") query = f""" - LOAD FROM "{jsonl_path}" (newline_delimited=true) + LOAD FROM "{jsonl_path}" RETURN fName AS name, age AS years """ result = self.conn.execute(query) @@ -875,7 +875,7 @@ def test_load_from_jsonl_with_column_alias(self): assert first_record[0] == "Alice", f"Expected 'Alice', got '{first_record[0]}'" assert first_record[1] == 35, f"Expected 35, got {first_record[1]}" - @json_test + @extension_test def test_load_from_jsonl_return_specific_columns(self): """Test LOAD FROM JSONL with column projection.""" jsonl_path = os.path.join(self.tinysnb_path, "json", "vPerson.jsonl") @@ -885,7 +885,7 @@ def test_load_from_jsonl_return_specific_columns(self): self.conn.execute("LOAD JSON") query = f""" - LOAD FROM "{jsonl_path}" (newline_delimited=true) + LOAD FROM "{jsonl_path}" RETURN fName, age """ result = self.conn.execute(query) @@ -900,7 +900,7 @@ def test_load_from_jsonl_return_specific_columns(self): assert isinstance(first_record[1], int), "age should be integer" print(first_record) - @json_test + @extension_test def test_load_from_jsonl_with_multiple_where_conditions(self): """Test LOAD FROM JSONL with multiple WHERE conditions.""" jsonl_path = os.path.join(self.tinysnb_path, "json", "vPerson.jsonl") @@ -911,7 +911,7 @@ def test_load_from_jsonl_with_multiple_where_conditions(self): # Test with multiple conditions: age > 25 AND age < 40 AND gender == 1 query = f""" - LOAD FROM "{jsonl_path}" (newline_delimited=true) + LOAD FROM "{jsonl_path}" WHERE age > 25 AND age < 40 AND gender = 1 RETURN fName, age, gender, eyeSight """ @@ -928,7 +928,7 @@ def test_load_from_jsonl_with_multiple_where_conditions(self): assert isinstance(fname, str), "fName should be string" assert isinstance(eye_sight, (int, float)), "eyeSight should be numeric" - @json_test + @extension_test def test_load_from_jsonl_with_complex_where_conditions(self): """Test LOAD FROM JSONL with complex WHERE conditions (age, eyeSight, height).""" jsonl_path = os.path.join(self.tinysnb_path, "json", "vPerson.jsonl") @@ -939,7 +939,7 @@ def test_load_from_jsonl_with_complex_where_conditions(self): # Test with multiple conditions: age >= 30 AND eyeSight >= 5.0 AND height > 1.0 query = f""" - LOAD FROM "{jsonl_path}" (newline_delimited=true) + LOAD FROM "{jsonl_path}" WHERE age >= 30 AND eyeSight >= 5.0 AND height > 1.0 RETURN fName, age, eyeSight, height """ @@ -1063,7 +1063,7 @@ def test_copy_from_node_with_column_remapping(self): assert records[0][2] == "Alice", "First person name should be Alice" assert records[0][3] == 1, "Alice's gender should be 1" - @json_test + @extension_test def test_copy_from_node_jsonl_with_column_remapping(self): """Test COPY FROM for node table with column remapping using JSONL file.""" jsonl_path = os.path.join(self.tinysnb_path, "json", "vPerson.jsonl") @@ -1093,7 +1093,7 @@ def test_copy_from_node_jsonl_with_column_remapping(self): # We want: ID, age, fName, gender, eyeSight, isStudent copy_query = f""" COPY person_jsonl_remap FROM ( - LOAD FROM "{jsonl_path}" (newline_delimited=true) + LOAD FROM "{jsonl_path}" RETURN ID, age, fName, gender, eyeSight, isStudent ) """ From b7354cd85dd8fa562679cf75f4b2d1a6a085c1b5 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Wed, 18 Mar 2026 14:27:19 +0800 Subject: [PATCH 42/60] remove bytearray --- proto/response.proto | 6 ------ 1 file changed, 6 deletions(-) diff --git a/proto/response.proto b/proto/response.proto index 4f3a1e4f..80e437ce 100644 --- a/proto/response.proto +++ b/proto/response.proto @@ -67,11 +67,6 @@ message BoolArray { bytes validity = 2; } -message BytesArray { - repeated bytes values = 1; - bytes validity = 2; -} - message ListArray { repeated uint32 offsets = 1 [packed = true]; Array elements = 2; @@ -126,7 +121,6 @@ message Array { DoubleArray double_array = 6; StringArray string_array = 7; BoolArray bool_array = 8; - BytesArray bytes_array = 9; TimestampArray timestamp_array = 10; DateArray date_array = 11; From 34b51b5925ffbed3f24605ad9fcf269b32d2ce9f Mon Sep 17 00:00:00 2001 From: BingqingLyu Date: Wed, 18 Mar 2026 17:10:13 +0800 Subject: [PATCH 43/60] add codegraph-qa skill (#78) --- skills/codegraph/SKILL.md | 389 ++++++++++++++++++++++++++++++ skills/codegraph/bug-analysis.md | 242 +++++++++++++++++++ skills/codegraph/evals/evals.json | 23 ++ skills/codegraph/patterns.md | 134 ++++++++++ skills/codegraph/schema.md | 54 +++++ 5 files changed, 842 insertions(+) create mode 100644 skills/codegraph/SKILL.md create mode 100644 skills/codegraph/bug-analysis.md create mode 100644 skills/codegraph/evals/evals.json create mode 100644 skills/codegraph/patterns.md create mode 100644 skills/codegraph/schema.md diff --git a/skills/codegraph/SKILL.md b/skills/codegraph/SKILL.md new file mode 100644 index 00000000..8a327e76 --- /dev/null +++ b/skills/codegraph/SKILL.md @@ -0,0 +1,389 @@ +--- +name: codegraph-qa +description: Use CodeScope to analyze any indexed codebase via its graph database (neug) and vector index (zvec). Supports Python, JavaScript/TypeScript, C, and Java (including Hadoop-scale repositories). Covers call graphs, dependency analysis, dead code detection, hotspots, module coupling, architectural layering, commit history, change attribution, semantic code search, impact analysis, full architecture reports, and bug root cause analysis from GitHub issues. Use this skill whenever the user asks about code structure, code dependencies, who calls what, why something changed, finding similar functions, generating architecture reports, understanding module boundaries, analyzing GitHub issues/bugs, finding bug root causes, understanding why a project has many bugs, tracing bugs to code, indexing Java projects, or any question that benefits from a code knowledge graph — even if they don't mention "CodeScope" by name. If a `.codegraph` or similar index directory exists in the workspace, this skill applies. +--- + +# CodeScope Q&A + +CodeScope indexes source code into a two-layer knowledge graph — **structure** (functions, calls, imports, classes, modules) and **evolution** (commits, file changes, function modifications) — plus **semantic embeddings** for every function. Supports **Python, JavaScript/TypeScript, C, and Java** (including Hadoop-scale repositories with 8K+ files). This combination enables analyses that grep, LSP, or pure vector search cannot do alone. It can also **fetch GitHub issues and trace bugs to code** — mapping bug reports to root cause candidates using the graph + vector infrastructure. + +## When to Use This Skill + +- User asks about call chains, callers, callees, or dependencies +- User wants to find dead code, hotspots, or architectural layers +- User asks about code history, who changed what, or why something was modified +- User wants to find semantically similar functions across a codebase +- User wants a full architecture analysis or report +- User asks about module coupling, circular dependencies, or bridge functions +- User wants to index or analyze a Java project (Maven, Gradle, plain Java) +- User wants to analyze GitHub issues or bug reports to find root causes +- User asks "why does this project have so many bugs" or "what code is most buggy" +- User wants to trace a bug report to the most relevant code locations +- A `.codegraph` directory (or similar index) exists in the workspace + +## Getting Started + +### Installation + +```bash +pip install codegraph-ai +``` + +### Environment Variables (optional) + +```bash +# Create Python virtural environment +python -m venv .venv + +source .venv/bin/activate + +# Point to a pre-built database (skip indexing) +export CODESCOPE_DB_DIR="/path/to/.linux_db" + +# Offline mode for HuggingFace models +export HF_HUB_OFFLINE="1" +``` + +### Check Index Status + +```bash +codegraph status --db $CODESCOPE_DB_DIR +``` + +If no index exists, create one: + +```bash +codegraph init --repo . --lang auto --commits 500 +``` + +Supported languages: `python`, `c`, `javascript`, `typescript`, `java`, or `auto` (auto-detects from file extensions). + +The `--commits` flag ingests git history (for evolution queries). Without it, only structural analysis is available. Add `--backfill-limit 200` to also compute function-level `MODIFIES` edges (slower but enables `change_attribution` and `co_change`). + +## Two Interfaces: CLI vs Python + +**Use the CLI** for status and reports: + +```bash +codegraph status --db $CODESCOPE_DB_DIR +codegraph analyze --db $CODESCOPE_DB_DIR --output report.md +``` + +**Use the Python API** for queries and custom analyses: + +```python +import os +os.environ['HF_HUB_OFFLINE'] = '1' # required + +from codegraph.core import CodeScope +cs = CodeScope(os.environ['CODESCOPE_DB_DIR']) + +# Cypher query +rows = list(cs.conn.execute(''' + MATCH (caller:Function)-[:CALLS]->(f:Function {name: "free_irq"}) + RETURN caller.name, caller.file_path LIMIT 10 +''')) +for r in rows: + print(r) + +cs.close() # always close when done +``` + +The Python API is more powerful — it gives you raw Cypher access and lets you chain queries. + +## Core Python API + +### Raw Queries + +These are the building blocks for any custom analysis: + +| Method | What it does | +|--------|-------------| +| `cs.conn.execute(cypher)` | Run any Cypher query against the graph — returns list of tuples | +| `cs.vector_only_search(query, topk=10)` | Semantic search over all function embeddings — returns `[{id, score}]` | +| `cs.summary()` | Print a human-readable overview of the indexed codebase | + +### Structural Analysis + +| Method | What it does | +|--------|-------------| +| `cs.impact(func_name, change_desc, max_hops=3)` | Find callers up to N hops, ranked by semantic relevance to the change | +| `cs.hotspots(topk=10)` | Rank functions by structural risk (fan-in × fan-out) | +| `cs.dead_code()` | Find functions with zero callers (excluding entry points) | +| `cs.circular_deps()` | Detect circular import chains at file level | +| `cs.module_coupling(topk=10)` | Find cross-module coupling pairs with call counts | +| `cs.bridge_functions(topk=30)` | Find functions called from the most distinct modules | +| `cs.layer_discovery(topk=30)` | Auto-discover infrastructure / mid / consumer layers | +| `cs.stability_analysis(topk=50)` | Correlate fan-in with modification frequency | +| `cs.class_hierarchy(class_name=None)` | Return inheritance tree for a class (or all classes) | + +### Semantic Search + +| Method | What it does | +|--------|-------------| +| `cs.similar(function, scope, topk=10)` | Find functions similar to a given function within a module scope | +| `cs.cross_locate(query, topk=10)` | Find semantically related functions, then reveal call-chain connections | +| `cs.semantic_cross_pollination(query, topk=15)` | Find similar functions across distant subsystems | + +### Evolution (requires `--commits` during init) + +| Method | What it does | +|--------|-------------| +| `cs.change_attribution(func_name, file_path=None, limit=20)` | Which commits modified a function? (requires backfill) | +| `cs.co_change(func_name, file_path=None, min_commits=2, topk=10)` | Functions that are always modified together | +| `cs.intent_search(query, topk=10)` | Find commits matching a natural-language intent | +| `cs.commit_modularity(topk=20)` | Score commits by how many modules they touch | +| `cs.hot_cold_map(topk=30)` | Module modification density | + +### Report Generation + +```python +from codegraph.analyzer import generate_report +report = generate_report(cs) # full architecture analysis as markdown +``` + +Or via CLI: + +```bash +codegraph analyze --output reports/analysis.md +``` + +The report covers: overview stats, subsystem distribution, top modules, architectural layers (with Mermaid diagrams), bridge functions, fan-in/fan-out hotspots, cross-module coupling, evolution hotspots, and dead code density. + +## Java Support + +CodeScope includes a full Java adapter that handles enterprise-scale repositories like Apache Hadoop (~8K files, ~97K functions indexed in ~3.5 minutes). + +### What Gets Indexed + +| Element | Graph Node/Edge | Notes | +|---------|----------------|-------| +| Classes | `Class` node | Includes generics, annotations | +| Interfaces | `Class` node | `extends` → `INHERITS` edge | +| Enums | `Class` node | Enum methods extracted | +| Methods | `Function` node | Full generic signatures, JavaDoc | +| Constructors | `Function` node (name=``) | Including `super()` calls | +| Method calls | `CALLS` edge | Receiver context preserved (`obj.method()`) | +| `new` expressions | `CALLS` edge to `ClassName.` | Constructor invocations | +| Imports | `IMPORTS` edge (file→file) | Single, wildcard, static | +| Inner classes | `Class` node (name=`Outer.Inner`) | Prefixed with outer class | +| Inheritance | `INHERITS` edge | `extends` + `implements` | + +### Indexing a Java Project + +```bash +codegraph init --repo /path/to/java-project --lang java --commits 500 +``` + +Or with auto-detection (auto-detects `.java` files): + +```bash +codegraph init --repo /path/to/java-project --lang auto +``` + +### Java-Specific Exclusions + +By default, these directories are excluded when indexing Java projects: `target/`, `build/`, `.gradle/`, `.idea/`, `.settings/`, `bin/`, `out/`, `test/`, `tests/`, `src/test/`. + +### Java Query Examples + +```python +# Find all classes that extend a specific class +list(cs.conn.execute(""" + MATCH (c:Class)-[:INHERITS]->(p:Class {name: 'FileSystem'}) + RETURN c.name, c.file_path +""")) + +# Find all methods in a specific class +list(cs.conn.execute(""" + MATCH (c:Class {name: 'DefaultParser'})-[:HAS_METHOD]->(f:Function) + RETURN f.name, f.signature +""")) + +# Find constructor call chains +list(cs.conn.execute(""" + MATCH (f:Function)-[:CALLS]->(init:Function {name: ''}) + WHERE init.class_name = 'Configuration' + RETURN f.name, f.file_path LIMIT 10 +""")) +``` + +## Bug Root Cause Analysis + +CodeScope can fetch GitHub issues and map them to code using the graph + vector infrastructure. This is the core workflow for answering questions like "why does this project have so many bugs?" or "where in the code does this bug come from?" + +### Prerequisites + +- A code graph must already be indexed for the target repository +- `gh` CLI must be installed and authenticated (`gh auth login`) + +### Bug Analysis API + +#### Single Issue Analysis + +```python +# Analyze a specific GitHub issue against the indexed code graph +result = cs.analyze_issue("owner", "repo", 1234, topk=10) +print(result.format_report()) +``` + +This: +1. Fetches the issue from GitHub (or loads from cache) +2. Parses file paths, function names, and stack traces from the issue body +3. Matches extracted paths to File nodes in the graph +4. Uses semantic search (`cross_locate`) to find related code +5. Traces callers of mentioned functions via `impact()` +6. Ranks and returns root cause candidates with explanation + +#### Batch Bug Analysis + +```python +# Analyze top-k bug issues and get aggregated hotspot data +results = cs.analyze_top_bugs("owner", "repo", k=10, label="bug") +for r in results: + print(f"#{r.issue.number}: {r.issue.title}") + for c in r.candidates[:3]: + print(f" {c.function_name} ({c.file_path}) score={c.score:.2f}") +``` + +#### CLI Commands + +```bash +# Fetch and parse a single issue (no graph needed) +codegraph fetch-issue owner repo 1234 + +# Fetch top-k bugs from a repo +codegraph fetch-bugs owner repo --top 10 --label bug + +# Analyze a single bug against the code graph +codegraph analyze-bug owner repo 1234 --db .codegraph --topk 10 + +# Batch analyze top bugs +codegraph analyze-bugs owner repo --db .codegraph --top 10 --label bug +``` + +#### Lower-Level Components + +For custom analysis pipelines, the components can be used individually: + +```python +from codegraph.issue_fetcher import fetch_and_parse_issue +from codegraph.bug_locator import ( + resolve_paths_to_files, + find_semantic_matches, + trace_callers, + rank_root_causes, + analyze_bug, +) + +# Fetch and parse (with caching) +issue = fetch_and_parse_issue("owner", "repo", 1234) +print(issue.extracted_paths) # file paths found in body +print(issue.extracted_funcs) # function names from stack traces +print(issue.linked_commits) # merge commit SHAs from linked PRs + +# Match paths to graph nodes +path_matches = resolve_paths_to_files(cs, issue.extracted_paths) + +# Semantic search using issue description +semantic_matches = find_semantic_matches(cs, f"{issue.title}\n{issue.body}") + +# Trace callers of mentioned functions +caller_traces = trace_callers(cs, issue.extracted_funcs, max_hops=2) + +# Combine into ranked candidates +candidates = rank_root_causes(path_matches, semantic_matches, caller_traces, issue.extracted_funcs) +``` + +### Scoring System + +Root cause candidates are scored by combining multiple signals: + +| Signal | Score | Description | +|--------|-------|-------------| +| Direct mention | +1.0 | Function name appears in issue body/stack trace | +| File path match | +0.8 | Function is in a file mentioned in the issue | +| Semantic match | +score | Raw cosine similarity (0.0-1.0) from `cross_locate` | +| Caller relationship | +0.5/hops | Function calls a mentioned function (decays with distance) | + +### Issue Cache + +Parsed issues are cached at `~/.codegraph/issue_cache/{owner}_{repo}_{number}.json`. Cache hits skip the GitHub API call entirely (sub-millisecond). To force a refresh, pass `use_cache=False` or use `--no-cache` on CLI. + +```python +from codegraph.issue_cache import clear_cache +clear_cache(owner="openclaw", repo="openclaw") # clear specific repo +clear_cache() # clear all +``` + +### Stack Trace Parsing + +The parser automatically extracts file paths and function names from stack traces in Python, C/C++, JavaScript/Node.js, Go, and Rust formats. It also extracts `func_name()` references in backticks and inline code. + +## How to Route Questions + +The key decision is: **does the user want an exact structural answer, a fuzzy semantic one, or a bug-to-code mapping?** + +| User asks... | Best approach | +|-------------|---------------| +| "Who calls `free_irq`?" | Cypher: `MATCH (c:Function)-[:CALLS]->(f:Function {name: 'free_irq'}) RETURN c.name, c.file_path` | +| "Find functions related to memory allocation" | `cs.vector_only_search("memory allocation")` or `cs.cross_locate("memory allocation")` | +| "What's the most complex function?" | `cs.hotspots(topk=1)` | +| "Is there dead code in the networking stack?" | `cs.dead_code()` then filter by file path | +| "How has `schedule()` changed recently?" | `cs.change_attribution("schedule", "kernel/sched/core.c")` | +| "Which modules are tightly coupled?" | `cs.module_coupling(topk=20)` | +| "Generate a full architecture report" | `codegraph analyze` or `generate_report(cs)` | +| "What's the architectural role of `mm/`?" | `cs.layer_discovery()` then find `mm` entries | +| "Which functions act as API boundaries?" | `cs.bridge_functions(topk=30)` | +| "Find commits about fixing race conditions" | `cs.intent_search("fix race condition")` | +| "What functions are always changed together with `kmalloc`?" | `cs.co_change("kmalloc")` | +| "Why does this project have so many bugs?" | `cs.analyze_top_bugs("owner", "repo", k=10)` then aggregate hotspots | +| "Analyze issue #1234 from GitHub" | `cs.analyze_issue("owner", "repo", 1234)` | +| "What code is related to this bug?" | `cs.analyze_issue(...)` or manual `cross_locate(bug_description)` | +| "Find the root cause of the crash in issue #42" | `cs.analyze_issue("owner", "repo", 42)` | +| "Which modules have the most bugs?" | `cs.analyze_top_bugs(...)` then aggregate by file/module | +| "Index this Java project" | `codegraph init --repo . --lang java` | +| "What classes extend FileSystem in Hadoop?" | Cypher: `MATCH (c:Class)-[:INHERITS]->(p:Class {name: 'FileSystem'}) RETURN c.name, c.file_path` | +| "Find all constructors called in this module" | Cypher: `MATCH (f:Function)-[:CALLS]->(init:Function {name: ''}) WHERE f.file_path CONTAINS 'module' RETURN ...` | + +For **novel investigations** not covered by pre-built methods, compose raw Cypher queries. See [patterns.md](./patterns.md) for templates. For bug analysis patterns, see [bug-analysis.md](./bug-analysis.md). + +## Important Filters for Cypher + +When writing Cypher queries, these filters prevent misleading results: + +- **`f.is_historical = 0`** — exclude deleted/renamed functions that are still in the graph as historical records +- **`f.is_external = 0`** (on File nodes) — exclude system headers/library files +- **`c.version_tag = 'bf'`** — only backfilled commits have `MODIFIES` edges; non-backfilled commits only have `TOUCHES` (file-level) edges +- **Always use `LIMIT`** — large codebases can return hundreds of thousands of rows + +## Checking Data Availability + +Before running evolution queries, check what's available: + +```python +# How many commits are indexed? +list(cs.conn.execute("MATCH (c:Commit) RETURN count(c)")) + +# How many have MODIFIES edges (backfilled)? +list(cs.conn.execute("MATCH (c:Commit) WHERE c.version_tag = 'bf' RETURN count(c)")) +``` + +If no commits exist, evolution methods will return empty results — guide the user to run `codegraph ingest` first. If commits exist but aren't backfilled, `TOUCHES` (file-level) queries still work but `MODIFIES` (function-level) queries won't. + +## Troubleshooting + +| Error | Cause | Fix | +|-------|-------|-----| +| `Database locked` | Crashed process left neug lock | `rm /graph.db/neugdb.lock` | +| `Can't open lock file` | zvec LOCK file deleted | `touch /vectors/LOCK` | +| `Can't lock read-write collection` | Another process holds lock | Kill the other process | +| `recovery idmap failed` | Stale WAL files | Remove empty `.log` files from `/vectors/idmap.0/` | + +The CLI auto-cleans lock issues on startup when possible. + +## References + +- **[schema.md](./schema.md)** — Full graph schema: node types, edge types, properties, Cypher syntax notes +- **[patterns.md](./patterns.md)** — Ready-to-use Cypher query templates and composition strategies +- **[bug-analysis.md](./bug-analysis.md)** — Bug analysis workflows: single issue, batch analysis, hotspot aggregation, custom pipelines diff --git a/skills/codegraph/bug-analysis.md b/skills/codegraph/bug-analysis.md new file mode 100644 index 00000000..00588cff --- /dev/null +++ b/skills/codegraph/bug-analysis.md @@ -0,0 +1,242 @@ +# Bug Analysis Workflows + +Patterns for tracing GitHub bugs to code using CodeScope's graph + vector infrastructure. + +## Table of Contents + +- [Quick Start](#quick-start) +- [Single Issue Analysis](#single-issue-analysis) +- [Batch Bug Hotspot Analysis](#batch-bug-hotspot-analysis) +- [Custom Analysis Pipelines](#custom-analysis-pipelines) +- [Combining Bug Analysis with Structural Analysis](#combining-bug-analysis-with-structural-analysis) + +## Quick Start + +```python +import os +os.environ['HF_HUB_OFFLINE'] = '1' + +from codegraph.core import CodeScope +cs = CodeScope(".codegraph") + +# "Why does this project have so many bugs?" +results = cs.analyze_top_bugs("owner", "repo", k=10, label="bug") +for r in results: + print(f"#{r.issue.number}: {r.issue.title}") + if r.candidates: + top = r.candidates[0] + print(f" -> {top.function_name} ({top.file_path})") + +cs.close() +``` + +## Single Issue Analysis + +### Basic Analysis + +```python +result = cs.analyze_issue("openclaw", "openclaw", 43608) +print(result.format_report()) +``` + +The result object (`BugAnalysisResult`) contains: + +| Field | Type | Description | +|-------|------|-------------| +| `issue` | `ParsedIssue` | Parsed issue with extracted paths/funcs/commits | +| `candidates` | `list[RootCauseCandidate]` | Ranked root cause locations | +| `path_matches` | `int` | How many extracted paths matched graph File nodes | +| `semantic_matches` | `int` | How many semantic matches were found | +| `caller_traces` | `int` | How many mentioned functions had traceable callers | +| `analysis_time_ms` | `float` | Total analysis time | + +### Inspecting the Parsed Issue + +```python +from codegraph.issue_fetcher import fetch_and_parse_issue + +issue = fetch_and_parse_issue("owner", "repo", 1234) + +# What the parser found in the issue body: +print(issue.extracted_paths) # ['src/handler.py', 'src/db.py'] +print(issue.extracted_funcs) # ['handle_request', 'execute_query'] +print(issue.extracted_locations) # [('src/handler.py', 42), ('src/db.py', 15)] +print(issue.linked_commits) # ['abc123...'] from linked PRs +print(issue.labels) # ['bug', 'regression'] +``` + +### Inspecting Candidates + +```python +for c in result.candidates: + print(f"{c.function_name} @ {c.file_path}") + print(f" Score: {c.score:.3f}") + print(f" Reasons: {c.reasons}") + # Reasons examples: + # "mentioned in issue" + # "in mentioned file src/handler.py" + # "semantic match (0.85)" + # "caller of handle_request (2 hops)" +``` + +## Batch Bug Hotspot Analysis + +### Find the Buggiest Code + +```python +results = cs.analyze_top_bugs("owner", "repo", k=10, label="bug") + +# Aggregate: which files appear across the most bug analyses? +file_counts = {} +func_counts = {} +module_counts = {} + +for r in results: + for c in r.candidates[:5]: # top 5 per bug + file_counts[c.file_path] = file_counts.get(c.file_path, 0) + 1 + func_counts[c.function_name] = func_counts.get(c.function_name, 0) + 1 + # module = first 2-3 path segments + parts = c.file_path.split("/") + module = "/".join(parts[:3]) if len(parts) >= 3 else c.file_path + module_counts[module] = module_counts.get(module, 0) + 1 + +print("Files with most bug associations:") +for f, n in sorted(file_counts.items(), key=lambda x: -x[1])[:10]: + print(f" {f}: {n} bugs") + +print("Functions with most bug associations:") +for f, n in sorted(func_counts.items(), key=lambda x: -x[1])[:10]: + print(f" {f}: {n} bugs") + +print("Modules with most bug associations:") +for m, n in sorted(module_counts.items(), key=lambda x: -x[1])[:10]: + print(f" {m}: {n} bugs") +``` + +### Cross-Reference with Structural Hotspots + +```python +# Are the buggiest functions also the riskiest (high fan-in x fan-out)? +hotspots = cs.hotspots(topk=50) +hotspot_names = {h.name for h in hotspots} + +buggy_and_risky = [f for f in func_counts if f in hotspot_names] +print(f"Functions that are both structurally risky AND frequently buggy:") +for f in buggy_and_risky: + print(f" {f}: {func_counts[f]} bugs, hotspot risk present") +``` + +## Custom Analysis Pipelines + +### Manual Bug-to-Code Mapping + +When you don't have a GitHub issue but have a bug description: + +```python +from codegraph.bug_locator import find_semantic_matches, trace_callers + +# Semantic search: find code related to the bug description +matches = find_semantic_matches(cs, "gateway crashes when processing messages", topk=10) +for m in matches: + print(f" {m['name']} ({m['file_path']}) score={m['score']:.2f}") + +# If you know which function is involved, trace its callers +callers = trace_callers(cs, ["handle_message", "process_data"], max_hops=2) +for t in callers: + print(f" Callers of {t['function']}:") + for c in t['callers']: + print(f" {c['name']} ({c['file']}, {c['hops']} hops)") +``` + +### Linking Bugs to Commits + +When issues have linked PRs with merge commits: + +```python +issue = fetch_and_parse_issue("owner", "repo", 1234) + +for sha in issue.linked_commits: + # Find what the fix commit modified + rows = list(cs.conn.execute(f""" + MATCH (c:Commit)-[:MODIFIES]->(f:Function) + WHERE c.hash STARTS WITH '{sha[:12]}' + RETURN f.name, f.file_path + """)) + if rows: + print(f"Commit {sha[:12]} modified:") + for name, path in rows: + print(f" {name} ({path})") +``` + +### Bug Pattern Detection + +Find if multiple bugs point to the same subsystem: + +```python +results = cs.analyze_top_bugs("owner", "repo", k=20, label="bug") + +# Group bugs by the module of their top candidate +module_bugs = {} +for r in results: + if r.candidates: + top = r.candidates[0] + parts = top.file_path.split("/") + module = "/".join(parts[:2]) + module_bugs.setdefault(module, []).append(r.issue.number) + +for module, bugs in sorted(module_bugs.items(), key=lambda x: -len(x[1])): + if len(bugs) >= 2: + print(f"{module}: {len(bugs)} bugs (#{', #'.join(str(b) for b in bugs)})") +``` + +## Combining Bug Analysis with Structural Analysis + +### "Is this bug in a risky part of the architecture?" + +```python +result = cs.analyze_issue("owner", "repo", 1234) +if result.candidates: + top = result.candidates[0] + # Check if the implicated function is a bridge function + bridges = cs.bridge_functions(topk=50) + bridge_names = {b.name for b in bridges} + if top.function_name in bridge_names: + print(f"Warning: {top.function_name} is a bridge function " + f"(called from many modules) — bug may have wide impact") + + # Check module coupling + couplings = cs.module_coupling(topk=20) + # ...examine if the implicated module is tightly coupled +``` + +### "What would break if we fix this bug?" + +```python +result = cs.analyze_issue("owner", "repo", 1234) +if result.candidates: + func = result.candidates[0].function_name + impacts = cs.impact(func, "bug fix", max_hops=3) + print(f"Fixing {func} could affect {len(impacts)} callers:") + for imp in impacts[:10]: + print(f" {imp.name} ({imp.file_path})") +``` + +## CLI Quick Reference + +```bash +# Fetch and inspect a single issue (no graph needed) +codegraph fetch-issue owner repo 1234 + +# Fetch top bugs from a repo +codegraph fetch-bugs owner repo --top 10 --label bug + +# Analyze a bug against indexed code +codegraph analyze-bug owner repo 1234 --db .codegraph + +# Batch analyze top bugs +codegraph analyze-bugs owner repo --db .codegraph --top 10 + +# Force refresh (skip cache) +codegraph fetch-issue owner repo 1234 --no-cache +codegraph analyze-bug owner repo 1234 --db .codegraph --no-cache +``` diff --git a/skills/codegraph/evals/evals.json b/skills/codegraph/evals/evals.json new file mode 100644 index 00000000..e0b5361c --- /dev/null +++ b/skills/codegraph/evals/evals.json @@ -0,0 +1,23 @@ +{ + "skill_name": "codegraph-qa", + "evals": [ + { + "id": 1, + "prompt": "I have pallets/click indexed in my .codegraph database. There's a bug report on GitHub — issue #3185 — about flag_value not working. Can you analyze it and find what code is most likely the root cause?", + "expected_output": "Should use cs.analyze_issue('pallets', 'click', 3185) and return root cause candidates related to flag_value/option processing in click's core code", + "files": [] + }, + { + "id": 2, + "prompt": "openclaw seems to have a lot of bugs. I've indexed the openclaw repo. Can you fetch the top 5 bug issues and tell me which parts of the codebase are most problematic?", + "expected_output": "Should use cs.analyze_top_bugs('openclaw', 'openclaw', k=5) and provide an aggregated hotspot summary showing which modules/files appear across multiple bugs", + "files": [] + }, + { + "id": 3, + "prompt": "I found a GitHub issue (openclaw/openclaw#43608) about cron tasks not executing even though the API returns success. The codebase is already indexed. Help me trace where the bug might be.", + "expected_output": "Should use cs.analyze_issue('openclaw', 'openclaw', 43608) and identify cron-related code (src/cron/, src/cli/cron-cli/) as root cause candidates", + "files": [] + } + ] +} diff --git a/skills/codegraph/patterns.md b/skills/codegraph/patterns.md new file mode 100644 index 00000000..e93a879e --- /dev/null +++ b/skills/codegraph/patterns.md @@ -0,0 +1,134 @@ +# CodeScope Cypher Patterns + +Run these via `cs.conn.execute(query)`. All queries return lists of tuples. + +## Structural Queries + +**Who calls a function?** +```cypher +MATCH (caller:Function)-[:CALLS]->(f:Function {name: 'free_irq'}) +RETURN caller.name, caller.file_path +ORDER BY caller.name LIMIT 30 +``` + +**What does a function call?** +```cypher +MATCH (f:Function {name: 'sched_fork'})-[:CALLS]->(callee:Function) +RETURN callee.name, callee.file_path +``` + +**Transitive callers (up to 3 hops):** +```cypher +MATCH (caller:Function)-[:CALLS*1..3]->(f:Function {name: 'kfree'}) +RETURN DISTINCT caller.name, caller.file_path LIMIT 50 +``` + +**Functions in a module:** +```cypher +MATCH (f:Function)<-[:DEFINES_FUNC]-(file:File)-[:BELONGS_TO]->(m:Module) +WHERE m.path_prefix = 'net/core' +RETURN f.name, file.path LIMIT 30 +``` + +**Cross-module calls (e.g. fs → mm):** +```cypher +MATCH (f1:Function)-[:CALLS]->(f2:Function), + (file1:File)-[:DEFINES_FUNC]->(f1), + (file2:File)-[:DEFINES_FUNC]->(f2), + (file1)-[:BELONGS_TO]->(m1:Module {path_prefix: 'fs'}), + (file2)-[:BELONGS_TO]->(m2:Module {path_prefix: 'mm'}) +RETURN f1.name, f2.name, count(*) AS calls +ORDER BY calls DESC LIMIT 20 +``` + +**Fan-in and fan-out:** +```cypher +MATCH (caller:Function)-[:CALLS]->(f:Function)-[:CALLS]->(callee:Function) +WHERE f.is_historical = 0 +WITH f, count(DISTINCT caller) AS fi, count(DISTINCT callee) AS fo +RETURN f.name, f.file_path, fi, fo, fi * fo AS risk +ORDER BY risk DESC LIMIT 20 +``` + +**Module sizes:** +```cypher +MATCH (f:Function)<-[:DEFINES_FUNC]-(file:File)-[:BELONGS_TO]->(m:Module) +WHERE f.is_historical = 0 +RETURN m.path_prefix, count(f) AS func_count +ORDER BY func_count DESC LIMIT 30 +``` + +**Functions by name pattern:** +```cypher +MATCH (f:Function) WHERE f.name STARTS WITH 'irq_' +RETURN f.name, f.file_path LIMIT 20 +``` + +```cypher +MATCH (f:Function) WHERE f.name CONTAINS 'alloc' +RETURN f.name, f.file_path LIMIT 30 +``` + +## Evolution Queries + +**Functions modified by a commit:** +```cypher +MATCH (c:Commit)-[:MODIFIES]->(f:Function) +WHERE c.hash STARTS WITH 'abc123' +RETURN f.name, f.file_path +``` + +**Commits touching a file:** +```cypher +MATCH (c:Commit)-[:TOUCHES]->(file:File {path: 'kernel/sched/core.c'}) +RETURN c.hash, c.message, c.author +``` + +**Co-changed functions (modified together frequently):** +```cypher +MATCH (c:Commit)-[:MODIFIES]->(f1:Function), + (c)-[:MODIFIES]->(f2:Function) +WHERE f1.id < f2.id +RETURN f1.name, f2.name, count(c) AS co_changes +ORDER BY co_changes DESC LIMIT 20 +``` + +**Largest commits (most functions changed):** +```cypher +MATCH (c:Commit)-[:MODIFIES]->(f:Function) +RETURN c.hash, c.message, count(f) AS funcs_changed +ORDER BY funcs_changed DESC LIMIT 10 +``` + +**Historical (deleted/renamed) functions:** +```cypher +MATCH (f:Function) WHERE f.is_historical = 1 +RETURN f.name, f.file_path LIMIT 30 +``` + +**Backfill progress:** +```cypher +MATCH (c:Commit) WHERE c.version_tag = 'bf' +RETURN count(c) AS backfilled +``` + +**Most frequently modified files:** +```cypher +MATCH (c:Commit)-[:TOUCHES]->(f:File) +RETURN f.path, count(c) AS commits +ORDER BY commits DESC LIMIT 20 +``` + +## Composition Strategies + +These patterns combine multiple query types for deeper insights: + +**Semantic + Structural**: Use `cs.vector_only_search("error recovery")` to find semantically similar functions, then query the graph to check if they share callers or modules. This reveals hidden architectural patterns. + +**Hypothesis Testing**: Query fan-in counts and MODIFIES counts for the same functions, then analyze correlation to validate architectural assumptions (e.g. "are the most-called functions also the most stable?"). + +**Evolution Forensics**: Find multi-module commits, examine which functions changed, and classify as refactoring vs feature vs bugfix by looking at how many modules and how many functions were touched. + +**Dependency Archaeology**: Find zero fan-in functions (dead code candidates), check `is_historical` for ghost functions, then trace which commits removed their callers — this tells you when and why code became dead. + +**Incremental Investigation**: Start with `cs.summary()` for high-level coverage, then check backfill state. If `MODIFIES` is sparse, use `TOUCHES`-based (file-level) analysis instead of function-level. diff --git a/skills/codegraph/schema.md b/skills/codegraph/schema.md new file mode 100644 index 00000000..05bc4e0f --- /dev/null +++ b/skills/codegraph/schema.md @@ -0,0 +1,54 @@ +# CodeScope Graph Schema + +## Nodes + +| Node | Key Properties | Notes | +|------|---------------|-------| +| `File` | id, path, language, loc, is_external | `is_external=1` for system headers / library stubs | +| `Function` | id, name, qualified_name, signature, file_path, start_line, end_line, doc_comment, class_name, is_historical | `is_historical=1` for deleted/renamed functions | +| `Class` | id, name, qualified_name, file_path | | +| `Module` | id, name, path_prefix | Auto-discovered from directories (e.g. `kernel/sched`) | +| `Commit` | id, hash, message, author, timestamp, version_tag | `version_tag='bf'` means MODIFIES edges computed | +| `Metadata` | id, value | Pipeline state (e.g. `oldest_commit`) | + +## Edges + +| Edge | From → To | Meaning | +|------|-----------|---------| +| `CALLS` | Function → Function | Static call graph (resolved from AST) | +| `DEFINES_FUNC` | File → Function | File defines this function | +| `DEFINES_CLASS` | File → Class | File defines this class | +| `HAS_METHOD` | Class → Function | Class contains this method | +| `IMPORTS` | File → File | Include / import dependency | +| `BELONGS_TO` | File → Module | File belongs to this module | +| `INHERITS` | Class → Class | Class inheritance | +| `MODIFIES` | Commit → Function | Commit changed this function (requires backfill) | +| `TOUCHES` | Commit → File | Commit changed this file (always present) | + +## Backfill State + +Not all commits have MODIFIES edges — only those with `version_tag = 'bf'`. TOUCHES edges are always present for all ingested commits. + +```cypher +MATCH (c:Commit) WHERE c.version_tag = 'bf' RETURN count(c) AS backfilled +``` + +```cypher +MATCH (c:Commit) RETURN count(c) AS total_commits +``` + +## Neug Cypher Reference + +**Supported syntax:** +- `MATCH`, `WHERE`, `RETURN`, `ORDER BY`, `LIMIT`, `WITH` +- Aggregations: `count()`, `count(DISTINCT x)` +- Inline property filters: `{name: 'foo'}` +- Variable-length paths: `[*1..3]` +- String predicates: `STARTS WITH`, `CONTAINS`, `ENDS WITH` +- Comparisons: `=`, `<>`, `<`, `>`, `<=`, `>=` +- Boolean: `AND`, `OR`, `NOT` + +**Limitations:** +- `OPTIONAL MATCH` is not available in the pip package +- Chained `MATCH` after `WITH` may be limited — prefer single `MATCH` clauses with multiple patterns separated by commas +- No `CREATE`, `SET`, `DELETE` via Cypher — graph mutations go through the Python API From 65d562563456122bbd5c70f64357bbef46176577 Mon Sep 17 00:00:00 2001 From: Zhang Lei Date: Thu, 19 Mar 2026 09:54:55 +0800 Subject: [PATCH 44/60] fix: Fix default value support for all type of properties (#63) Refactor the default value support for storage, avoid exposing default_value on column and mmap_array --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- include/neug/utils/id_indexer.h | 10 ++- include/neug/utils/mmap_array.h | 98 +++++++++++++++++++++------- include/neug/utils/property/column.h | 68 ++++++++++++++----- include/neug/utils/property/table.h | 12 ++-- src/storages/graph/edge_table.cc | 30 ++------- src/storages/graph/property_graph.cc | 4 -- src/storages/graph/vertex_table.cc | 5 +- src/utils/property/column.cc | 14 ++-- src/utils/property/table.cc | 61 +++++++++-------- tests/utils/test_table.cc | 66 +++++++++++++------ tools/python_bind/tests/test_ddl.py | 59 +++++++++++++++++ 11 files changed, 289 insertions(+), 138 deletions(-) diff --git a/include/neug/utils/id_indexer.h b/include/neug/utils/id_indexer.h index b31b37a1..a8acad2b 100644 --- a/include/neug/utils/id_indexer.h +++ b/include/neug/utils/id_indexer.h @@ -242,13 +242,11 @@ class LFIndexer { void init(const DataTypeId& type, std::shared_ptr extra_type_info = nullptr) { keys_ = nullptr; - auto default_value = get_default_value(type); switch (type) { -#define TYPE_DISPATCHER(enum_val, T) \ - case DataTypeId::enum_val: { \ - keys_ = std::make_shared>( \ - PropUtils::to_typed(default_value), StorageStrategy::kMem); \ - break; \ +#define TYPE_DISPATCHER(enum_val, T) \ + case DataTypeId::enum_val: { \ + keys_ = std::make_shared>(StorageStrategy::kMem); \ + break; \ } TYPE_DISPATCHER(kInt64, int64_t) TYPE_DISPATCHER(kInt32, int32_t) diff --git a/include/neug/utils/mmap_array.h b/include/neug/utils/mmap_array.h index b64eaa14..965ec30b 100644 --- a/include/neug/utils/mmap_array.h +++ b/include/neug/utils/mmap_array.h @@ -23,12 +23,15 @@ #include #include #include +#include +#include #include #include #include "glog/logging.h" #include "neug/storages/file_names.h" #include "neug/utils/exception/exception.h" +#include "neug/utils/file_utils.h" #ifdef __ia64__ #define ADDR (void*) (0x8000000000000000UL) @@ -66,6 +69,9 @@ inline size_t hugepage_round_up(size_t size) { return ROUND_UP(size); } namespace neug { +template +class TypedColumn; + enum class MemoryStrategy { kSyncToFile, kMemoryOnly, @@ -476,6 +482,7 @@ struct string_item { template <> class mmap_array { public: + friend class TypedColumn; mmap_array() {} mmap_array(mmap_array&& rhs) : mmap_array() { swap(rhs); } ~mmap_array() {} @@ -498,20 +505,17 @@ class mmap_array { } void open_with_hugepages(const std::string& filename) { + is_writable_ = true; items_.open_with_hugepages(filename + ".items"); data_.open_with_hugepages(filename + ".data"); } - void touch(const std::string& filename) { - items_.touch(filename + ".items"); - data_.touch(filename + ".data"); - } - void dump(const std::string& filename) { // Compact before dumping to reclaim unused space auto plan = prepare_compaction_plan(); + size_t effective_size = plan.total_size - plan.reused_size; bool should_stream = - !data_.is_sync_to_file() && plan.total_size < data_.size(); + !data_.is_sync_to_file() && effective_size < data_.size(); if (should_stream) { stream_compact_and_dump(plan, filename + ".data", filename + ".items"); return; @@ -520,6 +524,7 @@ class mmap_array { compact(); items_.dump(filename + ".items"); data_.dump(filename + ".data"); + reset(); } void resize(size_t size, size_t data_size) { @@ -545,7 +550,8 @@ class mmap_array { } void set(size_t idx, size_t offset, const std::string_view& val) { - items_.set(idx, {offset, static_cast(val.size())}); + items_.set(idx, {static_cast(offset), + static_cast(val.size())}); assert(data_.data() + offset + val.size() <= data_.data() + data_.size()); memcpy(data_.data() + offset, val.data(), val.size()); } @@ -562,6 +568,7 @@ class mmap_array { void swap(mmap_array& rhs) { items_.swap(rhs.items_); data_.swap(rhs.data_); + std::swap(is_writable_, rhs.is_writable_); } void set_writable(bool is_writable) { @@ -579,6 +586,7 @@ class mmap_array { is_writable_ = true; } + private: // Compact the data buffer by removing unused space and updating offsets // This is an in-place operation that shifts valid string data forward // Returns the compacted data size. Note that the reserved size of data buffer @@ -590,32 +598,41 @@ class mmap_array { return 0; } size_t size_before_compact = data_.size(); - if (plan.total_size == size_before_compact) { + if (plan.total_size == plan.reused_size + size_before_compact) { return size_before_compact; } + size_t effective_size = plan.total_size - plan.reused_size; - std::vector temp_buf(plan.total_size); + std::vector temp_buf(effective_size); size_t write_offset = 0; size_t limit_offset = 0; + std::unordered_map old_offset_to_new; for (const auto& entry : plan.entries) { - const char* src = data_.data() + entry.offset; - char* dst = temp_buf.data() + write_offset; - limit_offset = std::max(limit_offset, - static_cast(entry.offset + entry.length)); - memcpy(dst, src, entry.length); - items_.set(entry.index, - {static_cast(write_offset), entry.length}); + if (entry.length > 0) { + auto it = old_offset_to_new.find(entry.offset); + if (it != old_offset_to_new.end()) { + items_.set(entry.index, {it->second, entry.length}); + continue; + } + old_offset_to_new.insert({entry.offset, write_offset}); + const char* src = data_.data() + entry.offset; + char* dst = temp_buf.data() + write_offset; + limit_offset = std::max( + limit_offset, static_cast(entry.offset + entry.length)); + memcpy(dst, src, entry.length); + } + items_.set(entry.index, {static_cast(write_offset), + static_cast(entry.length)}); write_offset += entry.length; } - assert(write_offset == plan.total_size); - memcpy(data_.data(), temp_buf.data(), plan.total_size); + assert(write_offset + plan.reused_size == plan.total_size); + memcpy(data_.data(), temp_buf.data(), effective_size); - VLOG(1) << "Compaction completed. New data size: " << plan.total_size + VLOG(1) << "Compaction completed. New data size: " << effective_size << ", old data size: " << limit_offset; - return plan.total_size; + return effective_size; } - private: struct CompactionPlan { struct Entry { size_t index; @@ -624,15 +641,25 @@ class mmap_array { }; std::vector entries; size_t total_size = 0; + size_t reused_size = 0; }; CompactionPlan prepare_compaction_plan() const { CompactionPlan plan; plan.entries.reserve(items_.size()); + std::unordered_set seen_offsets; for (size_t i = 0; i < items_.size(); ++i) { const string_item& item = items_.get(i); plan.total_size += item.length; - plan.entries.push_back({i, item.offset, item.length}); + plan.entries.push_back( + {i, item.offset, static_cast(item.length)}); + if (item.length > 0) { + if (seen_offsets.find(item.offset) != seen_offsets.end()) { + plan.reused_size += item.length; + } else { + seen_offsets.insert(item.offset); + } + } } return plan; } @@ -651,10 +678,18 @@ class mmap_array { } size_t write_offset = 0; + std::unordered_map old_offset_to_new; for (const auto& entry : plan.entries) { if (entry.length > 0) { + auto it = old_offset_to_new.find(entry.offset); + if (it != old_offset_to_new.end()) { + items_.set(entry.index, {it->second, entry.length}); + continue; + } + old_offset_to_new.insert({entry.offset, write_offset}); const char* src = data_.data() + entry.offset; if (fwrite(src, 1, entry.length, fout) != entry.length) { + fclose(fout); std::stringstream ss; ss << "Failed to fwrite file [ " << data_filename << " ], " << strerror(errno); @@ -662,13 +697,14 @@ class mmap_array { THROW_RUNTIME_ERROR(ss.str()); } } - items_.set(entry.index, - {static_cast(write_offset), entry.length}); + items_.set(entry.index, {static_cast(write_offset), + static_cast(entry.length)}); write_offset += entry.length; } - assert(write_offset == plan.total_size); + assert(write_offset + plan.reused_size == plan.total_size); if (fflush(fout) != 0) { + fclose(fout); std::stringstream ss; ss << "Failed to fflush file [ " << data_filename << " ], " << strerror(errno); @@ -677,6 +713,7 @@ class mmap_array { } int fd = fileno(fout); if (fd == -1) { + fclose(fout); std::stringstream ss; ss << "Failed to get file descriptor for [ " << data_filename << " ], " << strerror(errno); @@ -684,6 +721,7 @@ class mmap_array { THROW_RUNTIME_ERROR(ss.str()); } if (ftruncate(fd, static_cast(size_before_compact)) != 0) { + fclose(fout); std::stringstream ss; ss << "Failed to ftruncate file [ " << data_filename << " ], " << strerror(errno); @@ -716,6 +754,16 @@ class mmap_array { items_.dump(items_filename); } + // Should only be used internally when we are sure the idx is valid + string_item get_string_item(size_t idx) const { return items_.get(idx); } + void set_string_item(size_t idx, const string_item& item) { + if (!is_writable_) { + THROW_RUNTIME_ERROR( + "Attempt to set_string_item on a read-only mmap_array"); + } + items_.set(idx, item); + } + mmap_array items_; mmap_array data_; bool is_writable_ = true; diff --git a/include/neug/utils/property/column.h b/include/neug/utils/property/column.h index 1566b4d5..9642b0d9 100644 --- a/include/neug/utils/property/column.h +++ b/include/neug/utils/property/column.h @@ -62,6 +62,7 @@ class ColumnBase { virtual void copy_to_tmp(const std::string& cur_path, const std::string& tmp_path) = 0; virtual void resize(size_t size) = 0; + virtual void resize(size_t size, const Property& default_value) = 0; virtual DataTypeId type() const = 0; @@ -88,8 +89,8 @@ class ColumnBase { template class TypedColumn : public ColumnBase { public: - explicit TypedColumn(const T& default_value, StorageStrategy strategy) - : default_value_(default_value), size_(0), strategy_(strategy) {} + explicit TypedColumn(StorageStrategy strategy) + : size_(0), strategy_(strategy) {} ~TypedColumn() { close(); } void open(const std::string& name, const std::string& snapshot_dir, @@ -154,11 +155,22 @@ class TypedColumn : public ColumnBase { size_t size() const override { return size_; } void resize(size_t size) override { + size_ = size; + buffer_.resize(size_); + } + + // Assume it is safe to insert the default value even if it is reserving, + // since user could always override + void resize(size_t size, const Property& default_value) override { + if (default_value.type() != type()) { + THROW_RUNTIME_ERROR("Default value type does not match column type"); + } size_t old_size = size_; size_ = size; buffer_.resize(size_); + auto default_typed_value = PropUtils::to_typed(default_value); for (size_t i = old_size; i < size_; ++i) { - buffer_.set(i, default_value_); + set_value(i, default_typed_value); } } @@ -206,7 +218,6 @@ class TypedColumn : public ColumnBase { } private: - T default_value_; mmap_array buffer_; size_t size_; StorageStrategy strategy_; @@ -241,6 +252,7 @@ class TypedColumn : public ColumnBase { void close() override {} size_t size() const override { return 0; } void resize(size_t size) override {} + void resize(size_t size, const Property& default_value) override {} DataTypeId type() const override { return DataTypeId::kEmpty; } @@ -265,24 +277,20 @@ class TypedColumn : public ColumnBase { StorageStrategy strategy_; }; -// No default value for StringColumn template <> class TypedColumn : public ColumnBase { public: - TypedColumn(StorageStrategy strategy, uint16_t width, - std::string_view default_value = "") + TypedColumn(StorageStrategy strategy, uint16_t width) : size_(0), pos_(0), strategy_(strategy), width_(width), - default_value_(default_value), type_(DataTypeId::kVarchar) {} explicit TypedColumn(StorageStrategy strategy) : size_(0), pos_(0), strategy_(strategy), width_(STRING_DEFAULT_MAX_LENGTH), - default_value_(""), type_(DataTypeId::kVarchar) {} TypedColumn(TypedColumn&& rhs) { buffer_.swap(rhs.buffer_); @@ -290,7 +298,6 @@ class TypedColumn : public ColumnBase { pos_ = rhs.pos_.load(); strategy_ = rhs.strategy_; width_ = rhs.width_; - default_value_ = rhs.default_value_; type_ = rhs.type_; } @@ -367,12 +374,42 @@ class TypedColumn : public ColumnBase { size_t avg_width = buffer_.avg_size(); // calculate average width of existing strings buffer_.resize( - size_, std::max(size_ * (avg_width > 0 ? avg_width - : STRING_DEFAULT_MAX_LENGTH), - pos_.load())); + size_, + std::max(size_ * (avg_width > 0 ? avg_width : width_), pos_.load())); + } else { + buffer_.resize(size_, std::max(size_ * width_, pos_.load())); + } + } + + void resize(size_t size, const Property& default_value) override { + if (default_value.type() != type()) { + THROW_RUNTIME_ERROR("Default value type does not match column type"); + } + std::unique_lock lock(rw_mutex_); + size_t old_size = size_; + size_ = size; + auto default_str = PropUtils::to_typed(default_value); + default_str = truncate_utf8(default_str, width_); + if (buffer_.size() != 0) { + size_t avg_width = + buffer_.avg_size(); // calculate average width of existing strings + buffer_.resize(size_, + std::max(size_ * (avg_width > 0 ? avg_width : width_), + pos_.load() + width_)); } else { buffer_.resize(size_, std::max(size_ * width_, pos_.load())); } + if (default_str.size() == 0) { + return; + } + + if (old_size < size_) { + set_value(old_size, default_str); + auto string_item = buffer_.get_string_item(old_size); + for (size_t i = old_size + 1; i < size_; ++i) { + buffer_.set_string_item(i, string_item); + } + } } DataTypeId type() const override { return type_; } @@ -443,21 +480,20 @@ class TypedColumn : public ColumnBase { pos_.store(0); } } + mmap_array buffer_; size_t size_; std::atomic pos_; StorageStrategy strategy_; std::shared_mutex rw_mutex_; uint16_t width_; - std::string_view default_value_; DataTypeId type_; }; using StringColumn = TypedColumn; std::shared_ptr CreateColumn( - DataType type, Property default_value, - StorageStrategy strategy = StorageStrategy::kMem); + DataType type, StorageStrategy strategy = StorageStrategy::kMem); /// Create RefColumn for ease of usage for hqps class RefColumnBase { diff --git a/include/neug/utils/property/table.h b/include/neug/utils/property/table.h index ed5fa119..c2c4af76 100644 --- a/include/neug/utils/property/table.h +++ b/include/neug/utils/property/table.h @@ -35,25 +35,21 @@ class Table { void init(const std::string& name, const std::string& work_dir, const std::vector& col_name, const std::vector& types, - const std::vector& default_property_values, const std::vector& strategies_); void open(const std::string& name, const std::string& work_dir, const std::vector& col_name, const std::vector& property_types, - const std::vector& default_property_values, const std::vector& strategies_); void open_in_memory(const std::string& name, const std::string& work_dir, const std::vector& col_name, const std::vector& property_types, - const std::vector& default_property_values, const std::vector& strategies_); void open_with_hugepages(const std::string& name, const std::string& work_dir, const std::vector& col_name, const std::vector& property_types, - const std::vector& default_property_values, const std::vector& strategies_, bool force = false); @@ -108,6 +104,12 @@ class Table { bool insert_safe = false); void resize(size_t row_num); + /** + * @brief Resize the table to row_num, and fill the new rows with default + * values. Assume it is safe to insert the default value even if it is + * reserving, since user could always override. + */ + void resize(size_t row_num, const std::vector& default_values); inline Property at(size_t row_id, size_t col_id) const { return column_ptrs_[col_id]->get_prop(row_id); @@ -130,12 +132,10 @@ class Table { void buildColumnPtrs(); void initColumns(const std::vector& col_name, const std::vector& types, - const std::vector& default_property_values, const std::vector& strategies_); std::unordered_map col_id_map_; std::vector col_names_; - std::vector col_default_values_; std::vector> columns_; std::vector column_ptrs_; diff --git a/src/storages/graph/edge_table.cc b/src/storages/graph/edge_table.cc index 2c70764c..b68acd4c 100644 --- a/src/storages/graph/edge_table.cc +++ b/src/storages/graph/edge_table.cc @@ -514,7 +514,7 @@ void EdgeTable::Open(const std::string& work_dir) { table_->open(edata_prefix(meta_->src_label_name, meta_->dst_label_name, meta_->edge_label_name), work_dir, meta_->property_names, meta_->properties, - meta_->default_property_values, meta_->strategies); + meta_->strategies); assert(table_->col_num() > 0); size_t table_cap = table_->get_column_by_id(0)->size(); load_statistic_file(work_dir, meta_->src_label_name, meta_->dst_label_name, @@ -543,8 +543,7 @@ void EdgeTable::OpenInMemory(const std::string& work_dir) { table_->open_in_memory( edata_prefix(meta_->src_label_name, meta_->dst_label_name, meta_->edge_label_name), - work_dir_, meta_->property_names, meta_->properties, - meta_->default_property_values, meta_->strategies); + work_dir_, meta_->property_names, meta_->properties, meta_->strategies); assert(table_->col_num() > 0); size_t table_cap = table_->get_column_by_id(0)->size(); load_statistic_file(work_dir, meta_->src_label_name, meta_->dst_label_name, @@ -574,7 +573,7 @@ void EdgeTable::OpenWithHugepages(const std::string& work_dir) { edata_prefix(meta_->src_label_name, meta_->dst_label_name, meta_->edge_label_name), checkpoint_dir_path, meta_->property_names, meta_->properties, - meta_->default_property_values, meta_->strategies, (memory_level_ > 2)); + meta_->strategies, (memory_level_ > 2)); assert(table_->col_num() > 0); size_t table_cap = table_->get_column_by_id(0)->size(); load_statistic_file(work_dir, meta_->src_label_name, meta_->dst_label_name, @@ -711,7 +710,7 @@ void EdgeTable::EnsureCapacity(size_t capacity) { return; } capacity = std::max(capacity, 4096UL); - table_->resize(capacity); + table_->resize(capacity, meta_->default_property_values); capacity_.store(capacity); } } @@ -1041,8 +1040,7 @@ void EdgeTable::dropAndCreateNewUnbundledCSR(bool delete_property) { LOG(INFO) << "rebuild unbundled edge csr with edge properties: " << meta_->property_names.size(); table_->open_in_memory(next_table_prefix, work_dir_, meta_->property_names, - meta_->properties, meta_->default_property_values, - meta_->strategies); + meta_->properties, meta_->strategies); } std::shared_ptr prev_data_col = nullptr; @@ -1060,26 +1058,10 @@ void EdgeTable::dropAndCreateNewUnbundledCSR(bool delete_property) { auto edges = out_csr_->batch_export(prev_data_col); if (prev_data_col && prev_data_col->size() > 0) { - table_->resize(prev_data_col->size()); + table_->resize(prev_data_col->size(), meta_->default_property_values); table_idx_.store(prev_data_col->size()); EnsureCapacity(prev_data_col->size()); } - // Set default value for other columns - for (size_t col_id = 1; col_id < table_->col_num(); ++col_id) { - auto col = table_->get_column_by_id(col_id); - if (col->type() == DataTypeId::kVarchar) { - VLOG(10) << "Skip set default value for column " << col_id - << " of type StringView"; - continue; - } - auto default_value = meta_->default_property_values[col_id]; - VLOG(10) << "Set default value for column " << col_id << ": " - << default_value.to_string() - << ", type: " << std::to_string(default_value.type()); - for (size_t row = 0; row < col->size(); ++row) { - col->set_any(row, default_value); - } - } std::vector row_ids; for (size_t i = 0; i < std::get<0>(edges).size(); ++i) { row_ids.push_back(i); diff --git a/src/storages/graph/property_graph.cc b/src/storages/graph/property_graph.cc index 242e56e0..391a1cbe 100644 --- a/src/storages/graph/property_graph.cc +++ b/src/storages/graph/property_graph.cc @@ -154,7 +154,6 @@ Status PropertyGraph::BatchAddEdges( return neug::Status::OK(); } -// TODO(zhanglei): support extra_type_info Status PropertyGraph::CreateVertexType( const std::string& vertex_type_name, const std::vector>& properties, @@ -260,7 +259,6 @@ Status PropertyGraph::CreateVertexType( return neug::Status::OK(); } -// TODO(zhanglei): support extra_type_info Status PropertyGraph::CreateEdgeType( const std::string& src_vertex_type, const std::string& dst_vertex_type, const std::string& edge_type_name, @@ -338,7 +336,6 @@ Status PropertyGraph::CreateEdgeType( return neug::Status::OK(); } -// TODO(zhanglei): Support extra_type_info Status PropertyGraph::AddVertexProperties( const std::string& vertex_type_name, const std::vector>& @@ -386,7 +383,6 @@ Status PropertyGraph::AddVertexProperties( return neug::Status::OK(); } -// TODO(zhanglei): Support extra_type_info Status PropertyGraph::AddEdgeProperties( const std::string& src_type_name, const std::string& dst_type_name, const std::string& edge_type_name, diff --git a/src/storages/graph/vertex_table.cc b/src/storages/graph/vertex_table.cc index dcc10d08..18de1fc5 100644 --- a/src/storages/graph/vertex_table.cc +++ b/src/storages/graph/vertex_table.cc @@ -34,7 +34,6 @@ void VertexTable::Open(const std::string& work_dir, int memory_level) { indexer_.open(indexer_filename, checkpoint_dir_path, work_dir_); table_->open(vertex_table_prefix(label_name), work_dir_, vertex_schema_->property_names, vertex_schema_->property_types, - vertex_schema_->default_property_values, vertex_schema_->storage_strategies); } else if (memory_level_ == 1) { @@ -42,7 +41,6 @@ void VertexTable::Open(const std::string& work_dir, int memory_level) { table_->open_in_memory(vertex_table_prefix(label_name), work_dir_, vertex_schema_->property_names, vertex_schema_->property_types, - vertex_schema_->default_property_values, vertex_schema_->storage_strategies); } else if (memory_level_ >= 2) { @@ -51,7 +49,6 @@ void VertexTable::Open(const std::string& work_dir, int memory_level) { table_->open_with_hugepages( vertex_table_prefix(label_name), work_dir_, vertex_schema_->property_names, vertex_schema_->property_types, - vertex_schema_->default_property_values, vertex_schema_->storage_strategies, (memory_level_ > 2)); } else { THROW_INTERNAL_EXCEPTION("Invalid memory level: " + @@ -188,7 +185,7 @@ size_t VertexTable::EnsureCapacity(size_t capacity) { indexer_.reserve(capacity); } if (table_ && table_->size() < capacity) { - table_->resize(capacity); + table_->resize(capacity, vertex_schema_->default_property_values); } v_ts_.Reserve(capacity); return indexer_.capacity(); diff --git a/src/utils/property/column.cc b/src/utils/property/column.cc index 01b3401c..2bbc7826 100644 --- a/src/utils/property/column.cc +++ b/src/utils/property/column.cc @@ -68,6 +68,7 @@ class TypedEmptyColumn : public ColumnBase { void close() override {} size_t size() const override { return 0; } void resize(size_t size) override {} + void resize(size_t size, const Property& default_value) override {} DataTypeId type() const override { return PropUtils::prop_type(); } @@ -108,6 +109,7 @@ class TypedEmptyColumn : public ColumnBase { void close() override {} size_t size() const override { return 0; } void resize(size_t size) override {} + void resize(size_t size, const Property& default_value) override {} DataTypeId type() const override { return DataTypeId::kVarchar; } @@ -132,7 +134,7 @@ class TypedEmptyColumn : public ColumnBase { void ensure_writable(const std::string& work_dir) override {} }; -std::shared_ptr CreateColumn(DataType type, Property default_value, +std::shared_ptr CreateColumn(DataType type, StorageStrategy strategy) { auto type_id = type.id(); auto extra_type_info = type.RawExtraTypeInfo(); @@ -151,10 +153,9 @@ std::shared_ptr CreateColumn(DataType type, Property default_value, } } else { switch (type_id) { -#define TYPE_DISPATCHER(enum_val, type) \ - case DataTypeId::enum_val: \ - return std::make_shared>( \ - PropUtils::to_typed(default_value), strategy); +#define TYPE_DISPATCHER(enum_val, type) \ + case DataTypeId::enum_val: \ + return std::make_shared>(strategy); FOR_EACH_DATA_TYPE_NO_STRING(TYPE_DISPATCHER) #undef TYPE_DISPATCHER case DataTypeId::kVarchar: { @@ -165,8 +166,7 @@ std::shared_ptr CreateColumn(DataType type, Property default_value, max_length = str_info->max_length; } } - return std::make_shared(strategy, max_length, - default_value.as_string_view()); + return std::make_shared(strategy, max_length); } case DataTypeId::kEmpty: { return std::make_shared>(strategy); diff --git a/src/utils/property/table.cc b/src/utils/property/table.cc index c3299b2b..53f9ab39 100644 --- a/src/utils/property/table.cc +++ b/src/utils/property/table.cc @@ -33,12 +33,10 @@ Table::~Table() { close(); } void Table::initColumns(const std::vector& col_name, const std::vector& property_types, - const std::vector& default_property_values, const std::vector& strategies_) { size_t col_num = col_name.size(); columns_.clear(); col_names_.clear(); - col_default_values_.clear(); col_id_map_.clear(); columns_.resize(col_num, nullptr); auto strategies = strategies_; @@ -49,11 +47,7 @@ void Table::initColumns(const std::vector& col_name, col_id_map_.insert({col_name[i], col_id}); col_names_.emplace_back(col_name[i]); assert(i < property_types.size()); - col_default_values_.emplace_back( - i < default_property_values.size() - ? default_property_values[i] - : get_default_value(property_types[i].id())); - columns_[col_id] = CreateColumn(property_types[i], col_default_values_[i]); + columns_[col_id] = CreateColumn(property_types[i]); } columns_.resize(col_id_map_.size()); } @@ -61,11 +55,10 @@ void Table::initColumns(const std::vector& col_name, void Table::init(const std::string& name, const std::string& work_dir, const std::vector& col_name, const std::vector& property_types, - const std::vector& default_property_values, const std::vector& strategies_) { name_ = name; work_dir_ = work_dir; - initColumns(col_name, property_types, default_property_values, strategies_); + initColumns(col_name, property_types, strategies_); for (size_t i = 0; i < columns_.size(); ++i) { columns_[i]->open(name + ".col_" + std::to_string(i), "", work_dir); } @@ -76,12 +69,11 @@ void Table::init(const std::string& name, const std::string& work_dir, void Table::open(const std::string& name, const std::string& work_dir, const std::vector& col_name, const std::vector& property_types, - const std::vector& default_property_values, const std::vector& strategies_) { name_ = name; work_dir_ = work_dir; snapshot_dir_ = checkpoint_dir(work_dir_); - initColumns(col_name, property_types, default_property_values, strategies_); + initColumns(col_name, property_types, strategies_); for (size_t i = 0; i < columns_.size(); ++i) { columns_[i]->open(name + ".col_" + std::to_string(i), snapshot_dir_, tmp_dir(work_dir)); @@ -93,12 +85,11 @@ void Table::open(const std::string& name, const std::string& work_dir, void Table::open_in_memory(const std::string& name, const std::string& work_dir, const std::vector& col_name, const std::vector& property_types, - const std::vector& default_property_values, const std::vector& strategies_) { name_ = name; work_dir_ = work_dir; snapshot_dir_ = checkpoint_dir(work_dir_); - initColumns(col_name, property_types, default_property_values, strategies_); + initColumns(col_name, property_types, strategies_); for (size_t i = 0; i < columns_.size(); ++i) { columns_[i]->open_in_memory(snapshot_dir_ + "/" + name + ".col_" + std::to_string(i)); @@ -107,16 +98,16 @@ void Table::open_in_memory(const std::string& name, const std::string& work_dir, buildColumnPtrs(); } -void Table::open_with_hugepages( - const std::string& name, const std::string& work_dir, - const std::vector& col_name, - const std::vector& property_types, - const std::vector& default_property_values, - const std::vector& strategies_, bool force) { +void Table::open_with_hugepages(const std::string& name, + const std::string& work_dir, + const std::vector& col_name, + const std::vector& property_types, + const std::vector& strategies_, + bool force) { name_ = name; work_dir_ = work_dir; snapshot_dir_ = checkpoint_dir(work_dir); - initColumns(col_name, property_types, default_property_values, strategies_); + initColumns(col_name, property_types, strategies_); for (size_t i = 0; i < columns_.size(); ++i) { columns_[i]->open_with_hugepages( snapshot_dir_ + "/" + name + ".col_" + std::to_string(i), force); @@ -159,9 +150,14 @@ void Table::reset_header(const std::vector& col_name) { void Table::add_columns(const std::vector& col_names, const std::vector& col_types, const std::vector& default_property_values, - size_t column_size, + size_t capacity, const std::vector& strategies_, int memory_level) { + if (default_property_values.size() != col_names.size()) { + THROW_RUNTIME_ERROR("default_property_values size mismatch: expected " + + std::to_string(col_names.size()) + " but got " + + std::to_string(default_property_values.size())); + } // When add_columns are called, the table is already initialized and col_files // are opened. std::stringstream ss; @@ -175,10 +171,9 @@ void Table::add_columns(const std::vector& col_names, int col_id = col_names_.size(); col_id_map_.insert({col_names[i], col_id}); col_names_.emplace_back(col_names[i]); - col_default_values_.emplace_back(default_property_values[i]); - columns_[col_id] = CreateColumn( - col_types[i], default_property_values[i], - i < strategies_.size() ? strategies_[i] : StorageStrategy::kMem); + columns_[col_id] = CreateColumn(col_types[i], i < strategies_.size() + ? strategies_[i] + : StorageStrategy::kMem); } for (size_t i = old_size; i < columns_.size(); ++i) { if (memory_level == 0) { @@ -190,7 +185,7 @@ void Table::add_columns(const std::vector& col_names, } else { THROW_NOT_IMPLEMENTED_EXCEPTION("Unsupported memory level"); } - columns_[i]->resize(column_size); + columns_[i]->resize(capacity, default_property_values[i - old_size]); } buildColumnPtrs(); } @@ -217,7 +212,6 @@ void Table::delete_column(const std::string& col_name) { columns_[col_id].reset(); columns_.erase(columns_.begin() + col_id); col_names_.erase(col_names_.begin() + col_id); - col_default_values_.erase(col_default_values_.begin() + col_id); for (size_t i = col_id; i < column_ptrs_.size() - 1; i++) { column_ptrs_[i] = column_ptrs_[i + 1]; } @@ -329,6 +323,19 @@ void Table::resize(size_t row_num) { } } +void Table::resize(size_t row_num, + const std::vector& default_values) { + if (default_values.size() != columns_.size()) { + THROW_RUNTIME_ERROR("default_values size mismatch: expected " + + std::to_string(columns_.size()) + " but got " + + std::to_string(default_values.size())); + } + for (size_t i = 0; i < columns_.size(); ++i) { + columns_[i]->ensure_writable(work_dir_); + columns_[i]->resize(row_num, default_values[i]); + } +} + void Table::ingest(uint32_t index, OutArchive& arc) { if (column_ptrs_.size() == 0) { return; diff --git a/tests/utils/test_table.cc b/tests/utils/test_table.cc index f02c843a..4e0e74ee 100644 --- a/tests/utils/test_table.cc +++ b/tests/utils/test_table.cc @@ -80,19 +80,6 @@ TEST(TableTest, TestTableBasic) { {DataTypeId::kTimestampMs}, {DataTypeId::kInterval}, {DataTypeId::kVarchar}}; - std::vector default_values = { - Property::from_bool(false), - Property::from_int32(0), - Property::from_uint32(0), - Property::from_int64(0), - Property::from_uint64(0), - Property::from_float(0.0), - Property::from_double(0.0), - Property::from_date(Date(0)), - Property::from_datetime(DateTime(0)), - Property::from_interval(Interval(std::string(""))), - Property::from_string_view("")}; - std::vector disk_strategies(col_name.size(), StorageStrategy::kDisk); std::vector mem_strategies(col_name.size(), @@ -101,11 +88,11 @@ TEST(TableTest, TestTableBasic) { StorageStrategy::kNone); disk_table.init("test_dist", TEST_DIR, col_name, property_types, - default_values, disk_strategies); + disk_strategies); mem_table.init("test_dist", TEST_DIR, col_name, property_types, - default_values, mem_strategies); + mem_strategies); none_table.init("test_dist", TEST_DIR, col_name, property_types, - default_values, none_strategies); + none_strategies); disk_table.resize(10); mem_table.resize(10); @@ -316,7 +303,7 @@ TEST(TableTest, TestTableBasic) { mem_table.drop(); disk_table.open("disk_table", std::string(TEST_DIR), col_name, property_types, - default_values, disk_strategies); + disk_strategies); EXPECT_EQ(disk_table.col_num(), 11); EXPECT_EQ(disk_table.get_column_by_id(0)->size(), 10); disk_table.reset_header(col_name); @@ -330,7 +317,7 @@ TEST(TableTest, TestTableBasic) { disk_table.drop(); mem_table.open_in_memory("disk_table", std::string(TEST_DIR), col_name, - property_types, default_values, mem_strategies); + property_types, mem_strategies); EXPECT_EQ(mem_table.col_num(), 11); EXPECT_EQ(mem_table.get_column_by_id(0)->size(), 10); const Table& mem_table_ref = mem_table; @@ -339,5 +326,46 @@ TEST(TableTest, TestTableBasic) { EXPECT_EQ(mem_table_ref.get_column_by_id(0)->type(), DataTypeId::kBoolean); } +TEST(TableTest, StringColumnDistinguishesUnsetFromEmptyString) { + if (std::filesystem::exists(TEST_DIR)) { + std::filesystem::remove_all(TEST_DIR); + } + std::filesystem::create_directories(TEST_DIR); + std::filesystem::create_directories(std::string(TEST_DIR) + "/checkpoint"); + std::filesystem::create_directories(std::string(TEST_DIR) + "/runtime/tmp"); + + Table table; + std::vector col_name = {"string_column"}; + std::vector property_types = {{DataTypeId::kVarchar}}; + std::vector mem_strategies(col_name.size(), + StorageStrategy::kMem); + + table.init("test_string_validity", TEST_DIR, col_name, property_types, + mem_strategies); + table.resize(2, {Property::from_string_view("default_value")}); + + auto string_column = std::dynamic_pointer_cast( + table.get_column("string_column")); + ASSERT_NE(string_column, nullptr); + + EXPECT_EQ(string_column->get_prop(0).as_string_view(), "default_value"); + + string_column->set_prop(1, Property::from_string_view("")); + EXPECT_TRUE(string_column->get_prop(1).as_string_view().empty()); + EXPECT_EQ(string_column->get_prop(1).type(), DataTypeId::kVarchar); + string_column->set_prop( + 1, Property::from_string_view("new value new value new value")); + EXPECT_EQ(string_column->get_prop(1).as_string_view(), + "new value new value new value"); + std::string path = std::string(TEST_DIR) + "/string_column"; + string_column->dump(path); + + StringColumn new_string_column(StorageStrategy::kMem); + new_string_column.open_in_memory(path); + EXPECT_EQ(new_string_column.get_prop(0).as_string_view(), "default_value"); + EXPECT_EQ(new_string_column.get_prop(1).as_string_view(), + "new value new value new value"); +} + } // namespace test -} // namespace neug \ No newline at end of file +} // namespace neug diff --git a/tools/python_bind/tests/test_ddl.py b/tools/python_bind/tests/test_ddl.py index 5b780d5f..8b9b63ea 100644 --- a/tools/python_bind/tests/test_ddl.py +++ b/tools/python_bind/tests/test_ddl.py @@ -360,3 +360,62 @@ def test_alter_varchar_type(): assert list(res) == [[1, "Alice"]] conn.close() db.close() + + +def test_get_varchar_default_value_1(): + db_dir = "/tmp/test_get_varchar_default_value_1" + shutil.rmtree(db_dir, ignore_errors=True) + db = Database(db_dir, "w") + conn = db.connect() + conn.execute( + "CREATE NODE TABLE TestNode(id INT64 PRIMARY KEY, name VARCHAR(20) DEFAULT 'default_name');" + ) + conn.execute("CREATE (:TestNode {id: 1});") + conn.execute("CREATE (:TestNode {id: 2});") + conn.execute("CREATE (:TestNode {id: 3});") + res = conn.execute("Match (n:TestNode) Return n.name;") + assert list(res) == [["default_name"], ["default_name"], ["default_name"]] + conn.close() + db.close() + + +def test_get_varchar_default_value_2(): + db_dir = "/tmp/test_get_varchar_default_value_2" + shutil.rmtree(db_dir, ignore_errors=True) + db = Database(db_dir, "w") + conn = db.connect() + conn.execute("CREATE NODE TABLE TestNode(id INT64 PRIMARY KEY);") + conn.execute("CREATE REL TABLE TestEdge(FROM TestNode TO TestNode);") + conn.execute("CREATE (:TestNode {id: 1});") + conn.execute("CREATE (:TestNode {id: 2});") + conn.execute("CREATE (:TestNode {id: 3});") + conn.execute( + "MATCH (a:TestNode {id: 1}), (b:TestNode {id: 2}) CREATE (a)-[:TestEdge]->(b);" + ) + conn.execute( + "MATCH (a:TestNode {id: 2}), (b:TestNode {id: 3}) CREATE (a)-[:TestEdge]->(b);" + ) + conn.execute("ALTER TABLE TestNode ADD name VARCHAR(20) DEFAULT 'default_name';") + conn.execute("CREATE (:TestNode {id: 4});") + conn.execute("CREATE (:TestNode {id: 5, name: 'custom_name'});") + res = conn.execute("Match (n:TestNode) Return n.name ORDER BY n.name;") + assert list(res) == [ + ["custom_name"], + ["default_name"], + ["default_name"], + ["default_name"], + ["default_name"], + ] + conn.execute("ALTER TABLE TestEdge ADD date INT64;") + conn.execute( + "MATCH (a:TestNode {id: 1})-[e:TestEdge]->(b:TestNode {id: 2}) SET e.date = 1234567890;" + ) + conn.execute( + "MATCH (a:TestNode {id: 1}), (b:TestNode { id: 3 }) CREATE (a)-[:TestEdge {date: 9876543210}]->(b);" + ) + res = conn.execute( + "MATCH (a:TestNode {id: 1})-[e:TestEdge]->(b:TestNode) RETURN e.date;" + ) + assert list(res) == [[1234567890], [9876543210]] + conn.close() + db.close() From fcf8e7c7651eac8518768779aa146daafb1d1741 Mon Sep 17 00:00:00 2001 From: Zhang Lei Date: Thu, 19 Mar 2026 11:57:57 +0800 Subject: [PATCH 45/60] fix: Fix incorrect edge table state when transforming between bundled and unbundled (#28) Fix incorrect edge table state when transforming between bundled and unbundled, include special case for string properties --- include/neug/storages/graph/edge_table.h | 2 +- src/storages/graph/edge_table.cc | 129 ++++++-- tests/storage/test_edge_table.cc | 392 ++++++++++++++++++++++- tests/storage/test_vertex_table.cc | 2 +- tools/python_bind/tests/test_ddl.py | 68 ++++ 5 files changed, 567 insertions(+), 26 deletions(-) diff --git a/include/neug/storages/graph/edge_table.h b/include/neug/storages/graph/edge_table.h index 1884eefd..3495461c 100644 --- a/include/neug/storages/graph/edge_table.h +++ b/include/neug/storages/graph/edge_table.h @@ -134,7 +134,7 @@ class EdgeTable { size_t Capacity() const; private: - void dropAndCreateNewBundledCSR(); + void dropAndCreateNewBundledCSR(std::shared_ptr prev_data_col); void dropAndCreateNewUnbundledCSR(bool delete_property); std::string get_next_csr_path_suffix(); diff --git a/src/storages/graph/edge_table.cc b/src/storages/graph/edge_table.cc index b68acd4c..44190afd 100644 --- a/src/storages/graph/edge_table.cc +++ b/src/storages/graph/edge_table.cc @@ -152,12 +152,58 @@ void batch_put_edges_with_default_edata(const std::vector& src_lid, case DataTypeId::kEmpty: batch_put_edges_with_default_edata_impl(src_lid, dst_lid, EmptyType(), out_csr); + break; default: THROW_NOT_SUPPORTED_EXCEPTION("not support edge data type: " + std::to_string(property_type)); } } +void batch_put_edges_to_bundled_csr(const std::vector& src_lid, + const std::vector& dst_lid, + DataTypeId property_type, + const std::vector& edge_data, + CsrBase* out_csr) { + switch (property_type) { +#define TYPE_DISPATCHER(enum_val, type) \ + case DataTypeId::enum_val: { \ + std::vector typed_data; \ + typed_data.reserve(edge_data.size()); \ + for (const auto& prop : edge_data) { \ + typed_data.emplace_back(PropUtils::to_typed(prop)); \ + } \ + dynamic_cast*>(out_csr)->batch_put_edges( \ + src_lid, dst_lid, typed_data); \ + break; \ + } + TYPE_DISPATCHER(kBoolean, bool); + TYPE_DISPATCHER(kInt32, int32_t); + TYPE_DISPATCHER(kUInt32, uint32_t); + TYPE_DISPATCHER(kInt64, int64_t); + TYPE_DISPATCHER(kUInt64, uint64_t); + TYPE_DISPATCHER(kFloat, float); + TYPE_DISPATCHER(kDouble, double); + TYPE_DISPATCHER(kDate, Date); + TYPE_DISPATCHER(kTimestampMs, DateTime); + TYPE_DISPATCHER(kInterval, Interval); +#undef TYPE_DISPATCHER + case DataTypeId::kEmpty: { + dynamic_cast*>(out_csr)->batch_put_edges( + src_lid, dst_lid, {}); + break; + } + case DataTypeId::kVarchar: { + THROW_NOT_SUPPORTED_EXCEPTION("not support edge data type: " + + std::to_string(property_type)); + break; + } + default: + THROW_NOT_SUPPORTED_EXCEPTION( + "Unsupported edge property type: " + + std::to_string(static_cast(property_type))); + } +} + template std::unique_ptr create_csr_impl(bool is_mutable, EdgeStrategy strategy) { @@ -191,7 +237,8 @@ static std::unique_ptr create_csr(bool is_mutable, return create_csr_impl(is_mutable, strategy); } default: { - LOG(FATAL) << "not support edge data type"; + THROW_NOT_SUPPORTED_EXCEPTION("not support edge data type: " + + std::to_string(property_type)); return nullptr; } } @@ -426,7 +473,8 @@ void batch_add_bundled_edges_impl(CsrBase* out_csr, CsrBase* in_csr, FOR_EACH_DATA_TYPE_NO_STRING(TYPE_DISPATCHER) #undef TYPE_DISPATCHER default: - LOG(FATAL) << "not support edge data type: " << prop_types[0].ToString(); + THROW_NOT_SUPPORTED_EXCEPTION("not support edge data type: " + + std::to_string(prop_types[0].id())); } } @@ -780,8 +828,9 @@ void EdgeTable::AddProperties(const std::vector& prop_names, if (table_->col_num() == 0) { // NOTE: Rather than check meta_->is_bundled(),we check whether the table // is empty. - if (meta_->properties.size() == 1) { - dropAndCreateNewBundledCSR(); + if (meta_->properties.size() == 1 && + meta_->properties[0].id() != DataTypeId::kVarchar) { + dropAndCreateNewBundledCSR(nullptr); } else { dropAndCreateNewUnbundledCSR(false); } @@ -822,6 +871,14 @@ void EdgeTable::DeleteProperties(const std::vector& col_names) { table_->delete_column(col); VLOG(1) << "delete column " << col; } + if (table_->col_num() == 0) { + dropAndCreateNewUnbundledCSR(true); + } else if (table_->col_num() == 1) { + auto remaining_col = table_->get_column_by_id(0); + if (remaining_col->type() != DataTypeId::kVarchar) { + dropAndCreateNewBundledCSR(remaining_col); + } + } } } @@ -834,9 +891,10 @@ int32_t EdgeTable::AddEdge(vid_t src_lid, vid_t dst_lid, (edge_data.size() == 0 && (meta_->properties.empty() || meta_->properties[0] == DataTypeId::kEmpty))); - in_csr_->put_generic_edge(dst_lid, src_lid, edge_data[0], ts, alloc); + Property bundled_data = edge_data.empty() ? Property() : edge_data[0]; + in_csr_->put_generic_edge(dst_lid, src_lid, bundled_data, ts, alloc); oe_offset = - out_csr_->put_generic_edge(src_lid, dst_lid, edge_data[0], ts, alloc); + out_csr_->put_generic_edge(src_lid, dst_lid, bundled_data, ts, alloc); } else { if (meta_->properties.size() != edge_data.size()) { THROW_INVALID_ARGUMENT_EXCEPTION( @@ -980,7 +1038,11 @@ size_t EdgeTable::Capacity() const { return capacity_.load(); } -void EdgeTable::dropAndCreateNewBundledCSR() { +void EdgeTable::dropAndCreateNewBundledCSR( + std::shared_ptr remaining_col) { + DataTypeId property_type = (remaining_col == nullptr) + ? meta_->properties[0].id() + : remaining_col->type(); auto suffix = get_next_csr_path_suffix(); std::string next_oe_csr_path = tmp_dir(work_dir_) + "/" + @@ -993,26 +1055,46 @@ void EdgeTable::dropAndCreateNewBundledCSR() { meta_->edge_label_name) + suffix; - auto edges = out_csr_->batch_export(nullptr); std::unique_ptr new_out_csr, new_in_csr; - assert(meta_->properties.size() == 1); - new_out_csr = create_csr(meta_->oe_mutable, meta_->oe_strategy, - meta_->properties[0].id()); - new_in_csr = create_csr(meta_->ie_mutable, meta_->ie_strategy, - meta_->properties[0].id()); + new_out_csr = + create_csr(meta_->oe_mutable, meta_->oe_strategy, property_type); + new_in_csr = create_csr(meta_->ie_mutable, meta_->ie_strategy, property_type); new_out_csr->open_in_memory(next_oe_csr_path); new_in_csr->open_in_memory(next_ie_csr_path); new_out_csr->resize(out_csr_->size()); new_in_csr->resize(in_csr_->size()); - batch_put_edges_with_default_edata( - std::get<0>(edges), std::get<1>(edges), meta_->properties[0].id(), - meta_->default_property_values[0], new_out_csr.get()); - batch_put_edges_with_default_edata( - std::get<1>(edges), std::get<0>(edges), meta_->properties[0].id(), - meta_->default_property_values[0], new_in_csr.get()); + if (remaining_col == nullptr) { + auto edges = out_csr_->batch_export(nullptr); + batch_put_edges_with_default_edata( + std::get<0>(edges), std::get<1>(edges), property_type, + meta_->default_property_values[0], new_out_csr.get()); + batch_put_edges_with_default_edata( + std::get<1>(edges), std::get<0>(edges), property_type, + meta_->default_property_values[0], new_in_csr.get()); + } else { + auto row_id_col = std::make_shared(StorageStrategy::kMem); + auto edges = out_csr_->batch_export(row_id_col); + std::vector remaining_data; + remaining_data.reserve(row_id_col->size()); + for (size_t i = 0; i < row_id_col->size(); ++i) { + auto row_id = row_id_col->get_view(i); + CHECK_LT(row_id, remaining_col->size()); + remaining_data.emplace_back(remaining_col->get_prop(row_id)); + } + batch_put_edges_to_bundled_csr(std::get<0>(edges), std::get<1>(edges), + property_type, remaining_data, + new_out_csr.get()); + batch_put_edges_to_bundled_csr(std::get<1>(edges), std::get<0>(edges), + property_type, remaining_data, + new_in_csr.get()); + } + table_->drop(); + table_ = std::make_unique(); + table_idx_.store(0); + capacity_.store(0); out_csr_->close(); in_csr_->close(); out_csr_ = std::move(new_out_csr); @@ -1046,7 +1128,9 @@ void EdgeTable::dropAndCreateNewUnbundledCSR(bool delete_property) { std::shared_ptr prev_data_col = nullptr; if (!delete_property) { - if (table_->col_num() >= 1) { + if (table_->col_num() >= 1 && + table_->get_column_by_id(0)->type() != DataTypeId::kVarchar && + table_->get_column_by_id(0)->type() != DataTypeId::kEmpty) { prev_data_col = table_->get_column_by_id(0); } } else { @@ -1061,12 +1145,15 @@ void EdgeTable::dropAndCreateNewUnbundledCSR(bool delete_property) { table_->resize(prev_data_col->size(), meta_->default_property_values); table_idx_.store(prev_data_col->size()); EnsureCapacity(prev_data_col->size()); + } else if (!delete_property) { + table_->resize(std::get<0>(edges).size(), meta_->default_property_values); + table_idx_.store(std::get<0>(edges).size()); + EnsureCapacity(std::get<0>(edges).size()); } std::vector row_ids; for (size_t i = 0; i < std::get<0>(edges).size(); ++i) { row_ids.push_back(i); } - std::unique_ptr new_out_csr, new_in_csr; if (delete_property) { new_out_csr = diff --git a/tests/storage/test_edge_table.cc b/tests/storage/test_edge_table.cc index 9b61c38a..91390288 100644 --- a/tests/storage/test_edge_table.cc +++ b/tests/storage/test_edge_table.cc @@ -26,6 +26,26 @@ namespace neug { namespace test { +class LocalGeneratedRecordBatchSupplier : public neug::IRecordBatchSupplier { + public: + explicit LocalGeneratedRecordBatchSupplier( + std::vector>&& batches) + : batch_index_(0), batches_(std::move(batches)) {} + + std::shared_ptr GetNextBatch() override { + if (batch_index_ >= batches_.size()) { + return nullptr; + } + auto batch = batches_[batch_index_]; + batch_index_++; + return batch; + } + + private: + size_t batch_index_ = 0; + std::vector> batches_; +}; + class EdgeTableTest : public ::testing::Test { protected: void SetUp() override { @@ -51,6 +71,10 @@ class EdgeTableTest : public ::testing::Test { "comment", {}, {}, {std::make_tuple(neug::DataTypeId::kInt64, "id", 0)}, {neug::StorageStrategy::kMem}, static_cast(1) << 32, "comment vertex label"); + schema_.AddEdgeLabel("person", "comment", "create0", {}, {}, {}, + neug::EdgeStrategy::kMultiple, + neug::EdgeStrategy::kMultiple, true, true, false, + "person creates comment edge without properties"); schema_.AddEdgeLabel( "person", "comment", "create1", {neug::DataTypeId::kInt32}, {"data"}, {neug::StorageStrategy::kMem}, neug::EdgeStrategy::kMultiple, @@ -70,6 +94,7 @@ class EdgeTableTest : public ::testing::Test { true, false, "person creates comment edge with two properties"); src_label_ = schema_.get_vertex_label_id("person"); dst_label_ = schema_.get_vertex_label_id("comment"); + edge_label_empty_ = schema_.get_edge_label_id("create0"); edge_label_int_ = schema_.get_edge_label_id("create1"); edge_label_str_ = schema_.get_edge_label_id("create2"); edge_label_str_int_ = schema_.get_edge_label_id("create3"); @@ -123,10 +148,34 @@ class EdgeTableTest : public ::testing::Test { void BatchInsert(std::vector>&& batches) { auto supplier = - std::make_shared(std::move(batches)); + std::make_shared(std::move(batches)); edge_table->BatchAddEdges(src_indexer, dst_indexer, supplier); } + size_t ExpectedBatchInsertCapacity(size_t inserted_edge_num) const { + if (inserted_edge_num == 0) { + return 0; + } + size_t new_cap = inserted_edge_num; + while (inserted_edge_num >= new_cap) { + new_cap = new_cap < 4096 ? 4096 : new_cap + (new_cap + 4) / 5; + } + return new_cap; + } + + void ExpectBundledStats(size_t expected_size) const { + ASSERT_NE(edge_table, nullptr); + EXPECT_EQ(edge_table->Size(), expected_size); + EXPECT_EQ(edge_table->Capacity(), neug::CsrBase::INFINITE_CAPACITY); + } + + void ExpectUnbundledStats(size_t expected_size, + size_t expected_capacity) const { + ASSERT_NE(edge_table, nullptr); + EXPECT_EQ(edge_table->Size(), expected_size); + EXPECT_EQ(edge_table->Capacity(), expected_capacity); + } + void OutputOutgoingEndpoints(std::vector& srcs, std::vector& dsts, neug::timestamp_t ts) { @@ -207,8 +256,8 @@ class EdgeTableTest : public ::testing::Test { neug::LFIndexer src_indexer; neug::LFIndexer dst_indexer; neug::Schema schema_; - neug::label_t src_label_, dst_label_, edge_label_int_, edge_label_str_, - edge_label_str_int_; + neug::label_t src_label_, dst_label_, edge_label_empty_, edge_label_int_, + edge_label_str_, edge_label_str_int_; std::string allocator_dir_; private: @@ -243,7 +292,9 @@ TEST_F(EdgeTableTest, TestBundledInt32) { this->InitIndexers(src_num, dst_num); this->ConstructEdgeTable(src_label_, dst_label_, edge_label_int_); this->OpenEdgeTable(); + this->ExpectBundledStats(0); this->BatchInsert(std::move(batches)); + this->ExpectBundledStats(edge_num); std::vector> input; for (size_t i = 0; i < edge_num; ++i) { @@ -313,7 +364,9 @@ TEST_F(EdgeTableTest, TestSeperatedString) { this->InitIndexers(src_num, dst_num); this->ConstructEdgeTable(src_label_, dst_label_, edge_label_str_); this->OpenEdgeTable(); + this->ExpectUnbundledStats(0, 0); this->BatchInsert(std::move(batches)); + this->ExpectUnbundledStats(edge_num, ExpectedBatchInsertCapacity(edge_num)); std::vector> input; for (size_t i = 0; i < edge_num; ++i) { @@ -386,7 +439,9 @@ TEST_F(EdgeTableTest, TestSeperatedIntString) { this->InitIndexers(src_num, dst_num); this->ConstructEdgeTable(src_label_, dst_label_, edge_label_str_int_); this->OpenEdgeTable(); + this->ExpectUnbundledStats(0, 0); this->BatchInsert(std::move(batches)); + this->ExpectUnbundledStats(edge_num, ExpectedBatchInsertCapacity(edge_num)); std::vector> input; for (size_t i = 0; i < edge_num; ++i) { @@ -445,6 +500,7 @@ TEST_F(EdgeTableTest, TestSeperatedIntString) { this->ConstructEdgeTable(src_label_, dst_label_, edge_label_str_int_); this->OpenEdgeTable(); + this->ExpectUnbundledStats(edge_num, ExpectedBatchInsertCapacity(edge_num)); { std::vector srcs, dsts; this->OutputOutgoingEndpoints(srcs, dsts, 0); @@ -519,6 +575,7 @@ TEST_F(EdgeTableTest, TestCountEdgeNum) { this->BatchInsert(std::move(batches)); EXPECT_EQ(this->edge_table->EdgeNum(), edge_num); + this->ExpectBundledStats(edge_num); } TEST_F(EdgeTableTest, TestDeleteEdge) { @@ -550,6 +607,7 @@ TEST_F(EdgeTableTest, TestDeleteEdge) { this->ConstructEdgeTable(src_label_, dst_label_, edge_label_int_); this->OpenEdgeTable(); this->BatchInsert(std::move(batches)); + this->ExpectBundledStats(edge_num); auto oe_view = this->edge_table->get_outgoing_view(neug::MAX_TIMESTAMP); auto ie_view = this->edge_table->get_incoming_view(neug::MAX_TIMESTAMP); @@ -579,6 +637,7 @@ TEST_F(EdgeTableTest, TestDeleteEdge) { this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); ASSERT_EQ(srcs.size(), edge_num - delete_count); ASSERT_EQ(dsts.size(), edge_num - delete_count); + this->ExpectBundledStats(edge_num - delete_count); // Test delete edge with soft delete, and revert. size_t soft_delete_todo = std::max(100, (int32_t) srcs.size() / 10); @@ -610,6 +669,8 @@ TEST_F(EdgeTableTest, TestDeleteEdge) { edge_num - delete_count - soft_deleted_edges.size()); ASSERT_EQ(tmp_dsts.size(), edge_num - delete_count - soft_deleted_edges.size()); + this->ExpectBundledStats(edge_num - delete_count - + soft_deleted_edges.size()); } // Revert soft deleted edges for (const auto& edge_record : soft_deleted_edges) { @@ -622,6 +683,7 @@ TEST_F(EdgeTableTest, TestDeleteEdge) { this->OutputOutgoingEndpoints(tmp_srcs, tmp_dsts, neug::MAX_TIMESTAMP); ASSERT_EQ(tmp_srcs.size(), edge_num - delete_count); ASSERT_EQ(tmp_dsts.size(), edge_num - delete_count); + this->ExpectBundledStats(edge_num - delete_count); } } @@ -643,8 +705,10 @@ TEST_F(EdgeTableTest, TestBatchAddEdgesBundled) { this->InitIndexers(src_num, dst_num); this->ConstructEdgeTable(src_label_, dst_label_, edge_label_int_); this->OpenEdgeTableInMemory(src_num, dst_num); + this->ExpectBundledStats(0); this->BatchInsert(std::move(batches)); EXPECT_EQ(this->edge_table->EdgeNum(), edge_num); + this->ExpectBundledStats(edge_num); // Generate more edges int64_t more_edge_num = 50; @@ -661,6 +725,7 @@ TEST_F(EdgeTableTest, TestBatchAddEdgesBundled) { // Insert more edges this->edge_table->BatchAddEdges(more_src_list, more_dst_list, edge_data); + this->ExpectBundledStats(edge_num + more_edge_num); std::vector srcs, dsts; this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); ASSERT_EQ(srcs.size(), edge_num + more_edge_num); @@ -688,8 +753,10 @@ TEST_F(EdgeTableTest, TestBatchAddEdgesUnbundled) { this->InitIndexers(src_num, dst_num); this->ConstructEdgeTable(src_label_, dst_label_, edge_label_str_int_); this->OpenEdgeTableInMemory(src_num, dst_num); + this->ExpectUnbundledStats(0, 0); this->BatchInsert(std::move(batches)); EXPECT_EQ(this->edge_table->EdgeNum(), edge_num); + this->ExpectUnbundledStats(edge_num, ExpectedBatchInsertCapacity(edge_num)); // Generate more edges int64_t more_edge_num = 50; @@ -708,6 +775,8 @@ TEST_F(EdgeTableTest, TestBatchAddEdgesUnbundled) { // Insert more edges this->edge_table->BatchAddEdges(more_src_list, more_dst_list, edge_data); + this->ExpectUnbundledStats(edge_num + more_edge_num, + ExpectedBatchInsertCapacity(edge_num)); std::vector srcs, dsts; this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); ASSERT_EQ(srcs.size(), edge_num + more_edge_num); @@ -741,6 +810,7 @@ TEST_F(EdgeTableTest, TestAddEdgeAndDelete) { } this->edge_table->EnsureCapacity(this->src_indexer.size(), this->dst_indexer.size()); + this->ExpectBundledStats(0); std::vector> edge_data; for (size_t i = 0; i < src_lids.size(); ++i) { edge_data.push_back({neug::Property::from_int32(static_cast(i))}); @@ -755,6 +825,7 @@ TEST_F(EdgeTableTest, TestAddEdgeAndDelete) { edge_count++; } EXPECT_EQ(edge_count, src_lids.size()); + this->ExpectBundledStats(edge_num); std::vector srcs, dsts; this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); ASSERT_EQ(srcs.size(), edge_num); @@ -788,6 +859,7 @@ TEST_F(EdgeTableTest, TestAddEdgeAndDelete) { this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); ASSERT_EQ(srcs.size(), edge_num - edges_to_delete.size()); ASSERT_EQ(dsts.size(), edge_num - edges_to_delete.size()); + this->ExpectBundledStats(edge_num - edges_to_delete.size()); srcs.clear(); dsts.clear(); this->OutputIncomingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); @@ -805,6 +877,7 @@ TEST_F(EdgeTableTest, TestAddEdgeAndDelete) { this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); ASSERT_EQ(srcs.size(), edge_num); ASSERT_EQ(dsts.size(), edge_num); + this->ExpectBundledStats(edge_num); srcs.clear(); dsts.clear(); this->OutputIncomingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); @@ -815,6 +888,7 @@ TEST_F(EdgeTableTest, TestAddEdgeAndDelete) { for (timestamp_t ts = 1; ts < 10; ++ts) { this->edge_table->AddEdge(0, 1, edge_data[0], ts, allocator); } + this->ExpectBundledStats(edge_num + 9); std::vector< std::pair, timestamp_t>> multi_edges_to_delete; @@ -843,6 +917,7 @@ TEST_F(EdgeTableTest, TestAddEdgeAndDelete) { for (auto it = es_after_delete.begin(); it != es_after_delete.end(); ++it) { EXPECT_FALSE(it.get_vertex() == 0 && it.get_timestamp() % 2 == 1); } + this->ExpectBundledStats(edge_num + 9 - multi_edges_to_delete.size()); for (const auto& pair : multi_edges_to_delete) { const auto& edge_record = pair.first; this->edge_table->RevertDeleteEdge( @@ -859,6 +934,7 @@ TEST_F(EdgeTableTest, TestAddEdgeAndDelete) { } } EXPECT_EQ(revert_count, multi_edges_to_delete.size()); + this->ExpectBundledStats(edge_num + 9); } TEST_F(EdgeTableTest, TestAddEdgeDeleteUnbundled) { @@ -888,6 +964,7 @@ TEST_F(EdgeTableTest, TestAddEdgeDeleteUnbundled) { } this->edge_table->EnsureCapacity(this->src_indexer.size(), this->dst_indexer.size()); + this->ExpectUnbundledStats(0, 0); std::vector> edge_data; for (size_t i = 0; i < src_lids.size(); ++i) { edge_data.push_back({neug::Property::from_string_view("edge_data"), @@ -898,12 +975,14 @@ TEST_F(EdgeTableTest, TestAddEdgeDeleteUnbundled) { size_t edge_count = 0; this->edge_table->EnsureCapacity(edge_data.size()); + this->ExpectUnbundledStats(0, 4096); for (size_t i = 0; i < src_lids.size(); ++i) { this->edge_table->AddEdge(src_lids[i], dst_lids[i], edge_data[i], 0, allocator); edge_count++; } EXPECT_EQ(edge_count, src_lids.size()); + this->ExpectUnbundledStats(edge_num, 4096); std::vector srcs, dsts; this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); ASSERT_EQ(srcs.size(), edge_num); @@ -935,6 +1014,7 @@ TEST_F(EdgeTableTest, TestAddEdgeDeleteUnbundled) { neug::MAX_TIMESTAMP); ASSERT_EQ(srcs_after_delete.size(), edge_num - deleted_edge_indices.size()); ASSERT_EQ(dsts_after_delete.size(), edge_num - deleted_edge_indices.size()); + this->ExpectUnbundledStats(edge_num, 4096); for (size_t i = 0, j = 0; i < edge_num; ++i) { if (j < deleted_edge_indices.size() && i == deleted_edge_indices[j]) { j++; @@ -971,6 +1051,7 @@ TEST_F(EdgeTableTest, TestEdgeTableCompaction) { } this->edge_table->EnsureCapacity(this->src_indexer.size(), this->dst_indexer.size()); + this->ExpectBundledStats(0); std::vector> edge_data; for (size_t i = 0; i < src_lids.size(); ++i) { edge_data.push_back({neug::Property::from_int32(static_cast(i))}); @@ -981,6 +1062,7 @@ TEST_F(EdgeTableTest, TestEdgeTableCompaction) { this->edge_table->AddEdge(src_lids[i], dst_lids[i], edge_data[i], 0, allocator); } + this->ExpectBundledStats(edge_num); auto oe_view = this->edge_table->get_outgoing_view(neug::MAX_TIMESTAMP); auto ie_view = this->edge_table->get_incoming_view(neug::MAX_TIMESTAMP); size_t delete_count = 0; @@ -1005,7 +1087,9 @@ TEST_F(EdgeTableTest, TestEdgeTableCompaction) { delete_count++; } } + this->ExpectBundledStats(edge_num - delete_count); this->edge_table->Compact(true, false, neug::MAX_TIMESTAMP); + this->ExpectBundledStats(edge_num - delete_count); size_t edge_count = 0; for (size_t i = 0; i < dst_lids.size(); ++i) { auto edges = ie_view.get_edges(dst_lids[i]); @@ -1042,6 +1126,7 @@ TEST_F(EdgeTableTest, TestUpdateEdgeData) { } this->edge_table->EnsureCapacity(this->src_indexer.size(), this->dst_indexer.size()); + this->ExpectUnbundledStats(0, 0); std::vector> edge_data; for (size_t i = 0; i < src_lids.size(); ++i) { edge_data.push_back({neug::Property::from_string_view("old_data"), @@ -1049,11 +1134,13 @@ TEST_F(EdgeTableTest, TestUpdateEdgeData) { } this->edge_table->EnsureCapacity(edge_data.size()); + this->ExpectUnbundledStats(0, 4096); neug::Allocator allocator(neug::MemoryStrategy::kMemoryOnly, allocator_dir_); for (size_t i = 0; i < src_lids.size(); ++i) { this->edge_table->AddEdge(src_lids[i], dst_lids[i], edge_data[i], 0, allocator); } + this->ExpectUnbundledStats(edge_num, 4096); std::vector new_data = { neug::Property::from_string_view("new_data"), neug::Property::from_int32(static_cast(1))}; @@ -1088,6 +1175,298 @@ TEST_F(EdgeTableTest, TestUpdateEdgeData) { } } +TEST_F(EdgeTableTest, TestAddPropertiesTransitionFromEmptyToBundledUnbundled) { + this->InitIndexers(4, 4); + this->ConstructEdgeTable(src_label_, dst_label_, edge_label_empty_); + this->OpenEdgeTableInMemory(4, 4); + this->ExpectBundledStats(0); + + std::vector> endpoints = {{0, 1}, {1, 2}, {2, 3}}; + std::vector src_list, dst_list; + for (const auto& [src_oid, dst_oid] : endpoints) { + src_list.emplace_back(src_oid); + dst_list.emplace_back(dst_oid); + } + auto src_arrs = convert_to_arrow_arrays(src_list, src_list.size()); + auto dst_arrs = convert_to_arrow_arrays(dst_list, dst_list.size()); + auto batches = + convert_to_record_batches({"src", "dst"}, {src_arrs, dst_arrs}); + this->BatchInsert(std::move(batches)); + this->ExpectBundledStats(endpoints.size()); + + schema_.AddEdgeProperties("person", "comment", "create0", {"weight"}, + {neug::DataTypeId::kInt32}, + {neug::Property::from_int32(7)}); + this->edge_table->SetEdgeSchema( + schema_.get_edge_schema(src_label_, dst_label_, edge_label_empty_)); + this->edge_table->AddProperties({"weight"}, {neug::DataTypeId::kInt32}, + {neug::Property::from_int32(7)}); + this->ExpectBundledStats(endpoints.size()); + + std::vector srcs, dsts; + this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); + ASSERT_EQ(srcs.size(), endpoints.size()); + std::vector weights; + this->OutputOutgoingEdgeData(weights, neug::MAX_TIMESTAMP, 0); + ASSERT_EQ(weights.size(), endpoints.size()); + for (auto weight : weights) { + EXPECT_EQ(weight, 7); + } + + schema_.AddEdgeProperties("person", "comment", "create0", {"tag"}, + {neug::DataTypeId::kVarchar}, + {neug::Property::from_string_view("new-tag")}); + this->edge_table->SetEdgeSchema( + schema_.get_edge_schema(src_label_, dst_label_, edge_label_empty_)); + this->edge_table->AddProperties( + {"tag"}, {neug::DataTypeId::kVarchar}, + {neug::Property::from_string_view("new-tag")}); + this->ExpectUnbundledStats(endpoints.size(), 4096); + + std::vector weights_after; + std::vector tags; + this->OutputOutgoingEdgeData(weights_after, neug::MAX_TIMESTAMP, 0); + this->OutputOutgoingEdgeData(tags, neug::MAX_TIMESTAMP, 1); + ASSERT_EQ(weights_after.size(), endpoints.size()); + ASSERT_EQ(tags.size(), endpoints.size()); + for (size_t i = 0; i < endpoints.size(); ++i) { + EXPECT_EQ(weights_after[i], 7); + EXPECT_EQ(tags[i], "new-tag"); + } +} + +TEST_F(EdgeTableTest, TestAddStringPropertyTransitionFromEmptyToUnbundled) { + this->InitIndexers(4, 4); + this->ConstructEdgeTable(src_label_, dst_label_, edge_label_empty_); + this->OpenEdgeTableInMemory(4, 4); + this->ExpectBundledStats(0); + + std::vector src_list = {0, 1, 2}; + std::vector dst_list = {1, 2, 3}; + auto src_arrs = convert_to_arrow_arrays(src_list, src_list.size()); + auto dst_arrs = convert_to_arrow_arrays(dst_list, dst_list.size()); + auto batches = + convert_to_record_batches({"src", "dst"}, {src_arrs, dst_arrs}); + this->BatchInsert(std::move(batches)); + this->ExpectBundledStats(src_list.size()); + + this->edge_table->SetEdgeSchema( + schema_.get_edge_schema(src_label_, dst_label_, edge_label_empty_)); + schema_.get_edge_schema(src_label_, dst_label_, edge_label_empty_) + ->add_properties({"tag"}, {neug::DataTypeId::kVarchar}, {}, + {neug::Property::from_string_view("seed")}); + this->edge_table->AddProperties({"tag"}, {neug::DataTypeId::kVarchar}, + {neug::Property::from_string_view("seed")}); + schema_.get_edge_schema(src_label_, dst_label_, edge_label_empty_) + ->add_properties({"desc"}, {neug::DataTypeId::kVarchar}, {}, + {neug::Property::from_string_view("unknown")}); + this->edge_table->AddProperties( + {"desc"}, {neug::DataTypeId::kVarchar}, + {neug::Property::from_string_view("unknown")}); + this->ExpectUnbundledStats(src_list.size(), 4096); + + std::vector tags, descs; + this->OutputOutgoingEdgeData(tags, neug::MAX_TIMESTAMP, 0); + this->OutputIncomingEdgeData(descs, neug::MAX_TIMESTAMP, 1); + ASSERT_EQ(tags.size(), src_list.size()); + for (auto tag : tags) { + EXPECT_EQ(tag, "seed"); + } + ASSERT_EQ(descs.size(), dst_list.size()); + for (auto desc : descs) { + EXPECT_EQ(desc, "unknown"); + } +} + +TEST_F(EdgeTableTest, + TestDeletePropertiesTransitionFromUnbundledToBundledEmpty) { + this->InitIndexers(4, 4); + this->ConstructEdgeTable(src_label_, dst_label_, edge_label_str_int_); + this->OpenEdgeTableInMemory(4, 4); + this->edge_table->EnsureCapacity(this->src_indexer.size(), + this->dst_indexer.size(), 100); + this->ExpectUnbundledStats(0, 4096); + + std::vector> input = { + {0, 1, "a", 11}, {1, 2, "b", 22}, {2, 3, "c", 33}}; + neug::Allocator allocator(neug::MemoryStrategy::kMemoryOnly, allocator_dir_); + for (const auto& [src_oid, dst_oid, data0, data1] : input) { + this->edge_table->AddEdge( + this->GetSrcLid(neug::Property::from_int64(src_oid)), + this->GetDstLid(neug::Property::from_int64(dst_oid)), + {neug::Property::from_string_view(data0), + neug::Property::from_int32(data1)}, + 0, allocator); + } + this->ExpectUnbundledStats(input.size(), 4096); + + this->edge_table->DeleteProperties({"data1"}); + schema_.DeleteEdgeProperties("person", "comment", "create3", {"data1"}); + this->edge_table->SetEdgeSchema( + schema_.get_edge_schema(src_label_, dst_label_, edge_label_str_int_)); + this->ExpectUnbundledStats(input.size(), 4096); + + std::vector srcs, dsts; + this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); + std::vector remaining_prop; + this->OutputOutgoingEdgeData(remaining_prop, + neug::MAX_TIMESTAMP, 0); + ASSERT_EQ(srcs.size(), input.size()); + ASSERT_EQ(remaining_prop.size(), input.size()); + std::vector> output; + for (size_t i = 0; i < srcs.size(); ++i) { + output.emplace_back(srcs[i], dsts[i], std::string(remaining_prop[i])); + } + std::sort(output.begin(), output.end()); + std::vector> expected; + for (const auto& [src_oid, dst_oid, data0, data1] : input) { + expected.emplace_back(src_oid, dst_oid, data0); + } + std::sort(expected.begin(), expected.end()); + EXPECT_EQ(output, expected); + + this->edge_table->DeleteProperties({"data0"}); + schema_.DeleteEdgeProperties("person", "comment", "create3", {"data0"}); + this->edge_table->SetEdgeSchema( + schema_.get_edge_schema(src_label_, dst_label_, edge_label_str_int_)); + this->ExpectBundledStats(input.size()); + + srcs.clear(); + dsts.clear(); + this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); + ASSERT_EQ(srcs.size(), input.size()); + ASSERT_EQ(dsts.size(), input.size()); + + this->edge_table->AddEdge(this->GetSrcLid(neug::Property::from_int64(3)), + this->GetDstLid(neug::Property::from_int64(0)), {}, + 0, allocator); + this->ExpectBundledStats(input.size() + 1); + srcs.clear(); + dsts.clear(); + this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); + ASSERT_EQ(srcs.size(), input.size() + 1); + ASSERT_EQ(dsts.size(), input.size() + 1); +} + +TEST_F(EdgeTableTest, TestDeletePropertiesTransitionFromUnbundledToBundled) { + this->InitIndexers(4, 4); + this->ConstructEdgeTable(src_label_, dst_label_, edge_label_str_int_); + this->OpenEdgeTableInMemory(4, 4); + this->edge_table->EnsureCapacity(this->src_indexer.size(), + this->dst_indexer.size(), 100); + this->ExpectUnbundledStats(0, 4096); + + std::vector> input = { + {0, 1, "a", 11}, {1, 2, "b", 22}, {2, 3, "c", 33}}; + neug::Allocator allocator(neug::MemoryStrategy::kMemoryOnly, allocator_dir_); + for (const auto& [src_oid, dst_oid, data0, data1] : input) { + this->edge_table->AddEdge( + this->GetSrcLid(neug::Property::from_int64(src_oid)), + this->GetDstLid(neug::Property::from_int64(dst_oid)), + {neug::Property::from_string_view(data0), + neug::Property::from_int32(data1)}, + 0, allocator); + } + this->ExpectUnbundledStats(input.size(), 4096); + + this->edge_table->DeleteProperties({"data0"}); + schema_.DeleteEdgeProperties("person", "comment", "create3", {"data0"}); + this->edge_table->SetEdgeSchema( + schema_.get_edge_schema(src_label_, dst_label_, edge_label_str_int_)); + + std::vector srcs, dsts; + this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); + std::vector remaining_prop; + this->OutputOutgoingEdgeData(remaining_prop, neug::MAX_TIMESTAMP, 0); + ASSERT_EQ(srcs.size(), input.size()); + ASSERT_EQ(remaining_prop.size(), input.size()); + std::vector> output; + for (size_t i = 0; i < srcs.size(); ++i) { + output.emplace_back(srcs[i], dsts[i], remaining_prop[i]); + } + std::sort(output.begin(), output.end()); + std::vector> expected; + for (const auto& [src_oid, dst_oid, data0, data1] : input) { + expected.emplace_back(src_oid, dst_oid, data1); + } + std::sort(expected.begin(), expected.end()); + EXPECT_EQ(output, expected); + + srcs.clear(); + dsts.clear(); + this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); + ASSERT_EQ(srcs.size(), input.size()); + ASSERT_EQ(dsts.size(), input.size()); + + this->ExpectBundledStats(input.size()); + this->edge_table->AddEdge(this->GetSrcLid(neug::Property::from_int64(3)), + this->GetDstLid(neug::Property::from_int64(0)), + {Property::from_int32(44)}, 0, allocator); + this->ExpectBundledStats(input.size() + 1); +} + +TEST_F(EdgeTableTest, TestAddAndDeletePropertiesStayUnbundled) { + this->InitIndexers(4, 4); + this->ConstructEdgeTable(src_label_, dst_label_, edge_label_str_int_); + this->OpenEdgeTableInMemory(4, 4); + this->edge_table->EnsureCapacity(this->src_indexer.size(), + this->dst_indexer.size(), 100); + this->ExpectUnbundledStats(0, 4096); + + std::vector> input = { + {0, 1, "a", 11}, {1, 2, "b", 22}, {2, 3, "c", 33}}; + neug::Allocator allocator(neug::MemoryStrategy::kMemoryOnly, allocator_dir_); + for (const auto& [src_oid, dst_oid, data0, data1] : input) { + this->edge_table->AddEdge( + this->GetSrcLid(neug::Property::from_int64(src_oid)), + this->GetDstLid(neug::Property::from_int64(dst_oid)), + {neug::Property::from_string_view(data0), + neug::Property::from_int32(data1)}, + 0, allocator); + } + this->ExpectUnbundledStats(input.size(), 4096); + + schema_.AddEdgeProperties("person", "comment", "create3", {"score"}, + {neug::DataTypeId::kInt32}, + {neug::Property::from_int32(99)}); + this->edge_table->SetEdgeSchema( + schema_.get_edge_schema(src_label_, dst_label_, edge_label_str_int_)); + this->edge_table->AddProperties({"score"}, {neug::DataTypeId::kInt32}, + {neug::Property::from_int32(99)}); + this->ExpectUnbundledStats(input.size(), 4096); + + std::vector score; + this->OutputOutgoingEdgeData(score, neug::MAX_TIMESTAMP, 2); + ASSERT_EQ(score.size(), input.size()); + for (auto value : score) { + EXPECT_EQ(value, 99); + } + + this->edge_table->DeleteProperties({"score"}); + schema_.DeleteEdgeProperties("person", "comment", "create3", {"score"}); + this->edge_table->SetEdgeSchema( + schema_.get_edge_schema(src_label_, dst_label_, edge_label_str_int_)); + this->ExpectUnbundledStats(input.size(), 4096); + + std::vector data0_after; + std::vector data1_after; + this->OutputOutgoingEdgeData(data0_after, + neug::MAX_TIMESTAMP, 0); + this->OutputOutgoingEdgeData(data1_after, neug::MAX_TIMESTAMP, 1); + ASSERT_EQ(data0_after.size(), input.size()); + ASSERT_EQ(data1_after.size(), input.size()); + std::vector> output; + std::vector srcs, dsts; + this->OutputOutgoingEndpoints(srcs, dsts, neug::MAX_TIMESTAMP); + for (size_t i = 0; i < srcs.size(); ++i) { + output.emplace_back(srcs[i], dsts[i], std::string(data0_after[i]), + data1_after[i]); + } + std::sort(output.begin(), output.end()); + std::sort(input.begin(), input.end()); + EXPECT_EQ(output, input); +} + template struct TypePair { using EdType = EDATA_T; @@ -1209,12 +1588,15 @@ TYPED_TEST(EdgeTableToolsTest, TestBatchAddEdges) { EdgeTable e_table = EdgeTable(edge_schema); e_table.BatchAddEdges(indexer, indexer, suppliers[0]); EXPECT_EQ(e_table.EdgeNum(), 10); + EXPECT_EQ(e_table.Size(), 10); + EXPECT_EQ(e_table.Capacity(), neug::CsrBase::INFINITE_CAPACITY); std::vector new_property_name = {"new_property"}; std::vector new_property_type = {DataTypeId::kInt32}; edge_schema->add_properties(new_property_name, new_property_type); e_table.AddProperties(new_property_name, new_property_type); EXPECT_EQ(e_table.PropertyNum(), 2); + EXPECT_EQ(e_table.Size(), 10); } TYPED_TEST(EdgeTableToolsTest, TestAddProperties) { @@ -1253,6 +1635,8 @@ TYPED_TEST(EdgeTableToolsTest, TestAddProperties) { EdgeTable e_table = EdgeTable(edge_schema); e_table.BatchAddEdges(indexer, indexer, suppliers[0]); EXPECT_EQ(e_table.EdgeNum(), 10); + EXPECT_EQ(e_table.Size(), 10); + EXPECT_EQ(e_table.Capacity(), neug::CsrBase::INFINITE_CAPACITY); if constexpr (std::is_same_v) { new_property_type = {DataTypeId::kInt32}; } else if constexpr (std::is_same_v) { @@ -1277,6 +1661,8 @@ TYPED_TEST(EdgeTableToolsTest, TestAddProperties) { edge_schema->add_properties(new_property_name, new_property_type); e_table.AddProperties(new_property_name, new_property_type); + EXPECT_EQ(e_table.Size(), 10); + EXPECT_EQ(e_table.Capacity(), neug::CsrBase::INFINITE_CAPACITY); } } // namespace test diff --git a/tests/storage/test_vertex_table.cc b/tests/storage/test_vertex_table.cc index 56a7a24e..45757065 100644 --- a/tests/storage/test_vertex_table.cc +++ b/tests/storage/test_vertex_table.cc @@ -811,4 +811,4 @@ TEST_F(VertexTableTest, VertexSetForeachVertex) { size_t count = 0; vset.foreach_vertex([&](neug::vid_t vid) { count++; }); EXPECT_EQ(count, 5); // Only odd lids are valid at ts=20 -} \ No newline at end of file +} diff --git a/tools/python_bind/tests/test_ddl.py b/tools/python_bind/tests/test_ddl.py index 8b9b63ea..9765039f 100644 --- a/tools/python_bind/tests/test_ddl.py +++ b/tools/python_bind/tests/test_ddl.py @@ -419,3 +419,71 @@ def test_get_varchar_default_value_2(): assert list(res) == [[1234567890], [9876543210]] conn.close() db.close() + + +def test_drop_add_edge_table_column(): + db_dir = "/tmp/test_drop_add_edge_table_column" + shutil.rmtree(db_dir, ignore_errors=True) + db = Database(db_dir, "w") + conn = db.connect() + # First create the graph schema + conn.execute( + """ + CREATE NODE TABLE IF NOT EXISTS TestNode( + id INT64 PRIMARY KEY, + thread_id INT64 + ) + """ + ) + conn.execute( + """ + CREATE REL TABLE IF NOT EXISTS TestEdge( + FROM TestNode TO TestNode + ) + """ + ) + conn.close() + db.close() + + db2 = Database(db_dir, "w") + conn2 = db2.connect() + conn2.execute("CREATE (v:TestNode {id: 1, thread_id: 1});") + conn2.execute("CREATE (v:TestNode {id: 2, thread_id: 2});") + conn2.execute("CREATE (v:TestNode {id: 3, thread_id: 3});") + conn2.execute( + "MATCH (v:TestNode {id: 1}), (v2:TestNode {id: 2}) CREATE (v)-[:TestEdge]->(v2);" + ) + conn2.execute( + "MATCH (v:TestNode {id: 1}), (v2:TestNode {id: 3}) CREATE (v)-[:TestEdge]->(v2);" + ) + conn2.execute("ALTER TABLE TestEdge ADD iteration INT64;") + conn2.execute( + "MATCH (v:TestNode {id: 1}), (v2:TestNode {id: 2}) CREATE (v)-[:TestEdge {iteration: 1}]->(v2);" + ) + ret = conn2.execute( + "MATCH (v1:TestNode)-[e:TestEdge]->(v2:TestNode) RETURN e.iteration;" + ) + assert list(ret) == [[0], [0], [1]] + conn2.execute("ALTER TABLE TestEdge DROP iteration;") + conn2.execute("ALTER TABLE TestEdge ADD iteration INT64;") + conn2.execute("ALTER TABLE TestEdge ADD iteration2 INT64;") + conn2.execute( + "MATCH (v:TestNode {id: 1}), (v2:TestNode {id: 2}) CREATE (v)-[:TestEdge {iteration: 2, iteration2: 3}]->(v2);" + ) + ret = conn2.execute( + "MATCH (v1:TestNode)-[e:TestEdge]->(v2:TestNode) RETURN e.iteration, e.iteration2;" + ) + assert list(ret) == [[0, 0], [0, 0], [0, 0], [2, 3]] + conn2.execute("ALTER TABLE TestEdge DROP iteration;") + conn2.execute("ALTER TABLE TestEdge DROP iteration2;") + # TODO(zhanglei): Turn on the test after issue #85 is fixed. + # conn2.execute("ALTER TABLE TestEdge ADD description STRING DEFAULT 'unknown';") + # conn2.execute( + # "MATCH (v:TestNode {id: 1}), (v2:TestNode {id: 2}) CREATE (v)-[:TestEdge {description: 'test'}]->(v2);" + # ) + # ret = conn2.execute( + # "MATCH (v1:TestNode)-[e:TestEdge]->(v2:TestNode) RETURN e.description ORDER BY e.description; " + # ) + # assert list(ret) == [["unknown"], ["unknown"], ["unknown"], ["unknown"], ["test"]] + conn2.close() + db2.close() From 839acf2ea0bc969a171135a25e6f1100e7448f80 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 19 Mar 2026 12:35:49 +0800 Subject: [PATCH 46/60] fix: make the dedup operator cover all column types (#80) * make dedup operator cover all column types * format * fix --- .../common/columns/arrow_context_column.h | 7 +- .../execution/common/columns/columns_utils.h | 10 +- .../execution/common/columns/edge_columns.h | 26 +++-- .../common/columns/i_context_column.h | 30 +++-- .../execution/common/columns/list_columns.h | 15 +-- .../execution/common/columns/path_columns.h | 5 +- .../execution/common/columns/struct_columns.h | 2 - .../execution/common/columns/value_columns.h | 6 +- .../execution/common/columns/vertex_columns.h | 18 ++- .../common/columns/arrow_context_column.cc | 110 ------------------ .../common/columns/struct_columns.cc | 4 - .../common/columns/vertex_columns.cc | 25 +++- .../common/operators/retrieve/dedup.cc | 5 +- .../common/operators/retrieve/unfold.cc | 2 +- 14 files changed, 85 insertions(+), 180 deletions(-) diff --git a/include/neug/execution/common/columns/arrow_context_column.h b/include/neug/execution/common/columns/arrow_context_column.h index 8e7514ea..32c8ba89 100644 --- a/include/neug/execution/common/columns/arrow_context_column.h +++ b/include/neug/execution/common/columns/arrow_context_column.h @@ -93,8 +93,6 @@ class ArrowArrayContextColumn : public IContextColumn { std::shared_ptr shuffle( const std::vector& offsets) const override; - void generate_dedup_offset(std::vector& offsets) const override; - std::shared_ptr cast_to_value_column() const; private: @@ -152,6 +150,11 @@ class ArrowStreamContextColumn : public IContextColumn { return suppliers_; } + Value get_elem(size_t idx) const override { + LOG(FATAL) << "get_elem not implemented for arrow stream column"; + return Value(DataType::SQLNULL); + } + private: std::shared_ptr first_batch_; std::vector> suppliers_; diff --git a/include/neug/execution/common/columns/columns_utils.h b/include/neug/execution/common/columns/columns_utils.h index 2dceaf8c..cbf07c29 100644 --- a/include/neug/execution/common/columns/columns_utils.h +++ b/include/neug/execution/common/columns/columns_utils.h @@ -25,10 +25,14 @@ namespace execution { class ColumnsUtils { public: template - static void generate_dedup_offset(const std::vector& vec, size_t row_num, + static void generate_dedup_offset(const std::vector& vec, std::vector& offsets) { - std::vector row_indices(row_num); - row_indices.resize(row_num); + std::vector row_indices(vec.size()); + if (vec.empty()) { + offsets.clear(); + return; + } + row_indices.resize(vec.size()); std::iota(row_indices.begin(), row_indices.end(), 0); std::sort(row_indices.begin(), row_indices.end(), [&vec](size_t a, size_t b) { diff --git a/include/neug/execution/common/columns/edge_columns.h b/include/neug/execution/common/columns/edge_columns.h index 59bede4c..75f9f653 100644 --- a/include/neug/execution/common/columns/edge_columns.h +++ b/include/neug/execution/common/columns/edge_columns.h @@ -78,9 +78,9 @@ class SDSLEdgeColumn : public IEdgeColumn { inline Direction dir() const override { return dir_; } - void generate_dedup_offset(std::vector& offsets) const override { - // TODO(liulexiao): dedup with property value - ColumnsUtils::generate_dedup_offset(edges_, size(), offsets); + bool generate_dedup_offset(std::vector& offsets) const override { + ColumnsUtils::generate_dedup_offset(edges_, offsets); + return true; } std::string column_info() const override { @@ -185,8 +185,9 @@ class MSEdgeColumn : public IEdgeColumn { inline size_t size() const override { return total_size_; } - void generate_dedup_offset(std::vector& offsets) const override { - LOG(FATAL) << "not implemented for " << this->column_info(); + bool generate_dedup_offset(std::vector& offsets) const override { + LOG(ERROR) << "not implemented for " << this->column_info(); + return false; } std::string column_info() const override { @@ -352,8 +353,9 @@ class BDSLEdgeColumn : public IEdgeColumn { inline size_t size() const override { return edges_.size(); } - void generate_dedup_offset(std::vector& offsets) const override { - ColumnsUtils::generate_dedup_offset(edges_, size(), offsets); + bool generate_dedup_offset(std::vector& offsets) const override { + ColumnsUtils::generate_dedup_offset(edges_, offsets); + return true; } std::string column_info() const override { @@ -459,8 +461,9 @@ class SDMLEdgeColumn : public IEdgeColumn { inline size_t size() const override { return edges_.size(); } - void generate_dedup_offset(std::vector& offsets) const override { - ColumnsUtils::generate_dedup_offset(edges_, size(), offsets); + bool generate_dedup_offset(std::vector& offsets) const override { + ColumnsUtils::generate_dedup_offset(edges_, offsets); + return true; } std::string column_info() const override { @@ -580,8 +583,9 @@ class BDMLEdgeColumn : public IEdgeColumn { inline size_t size() const override { return edges_.size(); } - void generate_dedup_offset(std::vector& offsets) const override { - ColumnsUtils::generate_dedup_offset(edges_, size(), offsets); + bool generate_dedup_offset(std::vector& offsets) const override { + ColumnsUtils::generate_dedup_offset(edges_, offsets); + return true; } std::string column_info() const override { diff --git a/include/neug/execution/common/columns/i_context_column.h b/include/neug/execution/common/columns/i_context_column.h index f799ac63..adcfcd33 100644 --- a/include/neug/execution/common/columns/i_context_column.h +++ b/include/neug/execution/common/columns/i_context_column.h @@ -45,10 +45,7 @@ class IContextColumn { IContextColumn() = default; virtual ~IContextColumn() = default; - virtual size_t size() const { - LOG(FATAL) << "not implemented for " << this->column_info(); - return 0; - } + virtual size_t size() const = 0; virtual std::string column_info() const = 0; virtual ContextColumnType column_type() const = 0; @@ -57,46 +54,47 @@ class IContextColumn { virtual std::shared_ptr shuffle( const std::vector& offsets) const { - LOG(FATAL) << "not implemented for " << this->column_info(); + LOG(FATAL) << "shuffle not implemented for " << this->column_info(); return nullptr; } virtual std::shared_ptr optional_shuffle( const std::vector& offsets) const { - LOG(FATAL) << "not implemented for " << this->column_info(); + LOG(FATAL) << "optional_shuffle not implemented for " + << this->column_info(); return nullptr; } virtual std::shared_ptr union_col( std::shared_ptr other) const { - LOG(FATAL) << "not implemented for " << this->column_info(); + LOG(FATAL) << "union_col not implemented for " << this->column_info(); return nullptr; } - virtual Value get_elem(size_t idx) const { - LOG(FATAL) << "not implemented for " << this->column_info(); - return Value(elem_type()); - } - + virtual Value get_elem(size_t idx) const = 0; virtual bool has_value(size_t idx) const { return true; } virtual bool is_optional() const { return false; } - virtual void generate_dedup_offset(std::vector& offsets) const { - LOG(FATAL) << "not implemented for " << this->column_info(); + virtual bool generate_dedup_offset(std::vector& offsets) const { + LOG(ERROR) << "generate_dedup_offset not implemented for " + << this->column_info() << ", return false by default"; + return false; } virtual std::pair, std::vector>> generate_aggregate_offset() const { - LOG(INFO) << "not implemented for " << this->column_info(); + LOG(INFO) << "generate_aggregate_offset not implemented for " + << this->column_info() << ", return empty by default"; std::shared_ptr col(nullptr); return std::make_pair(col, std::vector>()); } virtual bool order_by_limit(bool asc, size_t limit, std::vector& offsets) const { - LOG(INFO) << "order by limit not implemented for " << this->column_info(); + LOG(ERROR) << "order by limit not implemented for " << this->column_info() + << ", return false by default"; return false; } }; diff --git a/include/neug/execution/common/columns/list_columns.h b/include/neug/execution/common/columns/list_columns.h index ba3332ea..6d9c2bc1 100644 --- a/include/neug/execution/common/columns/list_columns.h +++ b/include/neug/execution/common/columns/list_columns.h @@ -19,12 +19,6 @@ namespace neug { namespace execution { -class ListColumnBase : public IContextColumn { - public: - virtual std::pair, std::vector> - unfold() const = 0; -}; - struct list_item { uint64_t offset; uint64_t length; @@ -32,7 +26,7 @@ struct list_item { class ListColumnBuilder; -class ListColumn : public ListColumnBase { +class ListColumn : public IContextColumn { public: explicit ListColumn(DataType type) : elem_type_(type) { std::shared_ptr elem_type_info = @@ -64,13 +58,8 @@ class ListColumn : public ListColumnBase { return Value::LIST(elem_type_, std::move(list_values)); } - void generate_dedup_offset(std::vector& offsets) const override { - LOG(FATAL) << "not implemented for " << this->column_info(); - // ColumnsUtils::generate_dedup_offset(data_, data_.size(), offsets); - } - std::pair, std::vector> unfold() - const override; + const; std::shared_ptr data_column() const { return datas_; } diff --git a/include/neug/execution/common/columns/path_columns.h b/include/neug/execution/common/columns/path_columns.h index 89b4efaf..506910c1 100644 --- a/include/neug/execution/common/columns/path_columns.h +++ b/include/neug/execution/common/columns/path_columns.h @@ -47,8 +47,9 @@ class PathColumn : public IContextColumn { } inline const Path& get_path(size_t idx) const { return data_[idx]; } - void generate_dedup_offset(std::vector& offsets) const override { - ColumnsUtils::generate_dedup_offset(data_, data_.size(), offsets); + bool generate_dedup_offset(std::vector& offsets) const override { + ColumnsUtils::generate_dedup_offset(data_, offsets); + return true; } template diff --git a/include/neug/execution/common/columns/struct_columns.h b/include/neug/execution/common/columns/struct_columns.h index 6c4dc760..050fdaed 100644 --- a/include/neug/execution/common/columns/struct_columns.h +++ b/include/neug/execution/common/columns/struct_columns.h @@ -49,8 +49,6 @@ class StructColumn : public IContextColumn { const DataType& elem_type() const override { return type_; } Value get_elem(size_t idx) const override; - void generate_dedup_offset(std::vector& offsets) const override; - bool is_optional() const override { return is_optional_; } bool has_value(size_t idx) const override { diff --git a/include/neug/execution/common/columns/value_columns.h b/include/neug/execution/common/columns/value_columns.h index 742f0ce8..790087c6 100644 --- a/include/neug/execution/common/columns/value_columns.h +++ b/include/neug/execution/common/columns/value_columns.h @@ -60,9 +60,10 @@ class ValueColumn : public IContextColumn { const std::vector& data() const { return data_; } const std::vector& validity_bitmap() const { return valid_; } - void generate_dedup_offset(std::vector& offsets) const override { + bool generate_dedup_offset(std::vector& offsets) const override { if (!is_optional_) { - return ColumnsUtils::generate_dedup_offset(data_, data_.size(), offsets); + ColumnsUtils::generate_dedup_offset(data_, offsets); + return true; } std::set st; size_t null_index = std::numeric_limits::max(); @@ -79,6 +80,7 @@ class ValueColumn : public IContextColumn { if (null_index != std::numeric_limits::max()) { offsets.push_back(null_index); } + return true; } std::shared_ptr union_col( diff --git a/include/neug/execution/common/columns/vertex_columns.h b/include/neug/execution/common/columns/vertex_columns.h index 63b57f6b..d0e7357f 100644 --- a/include/neug/execution/common/columns/vertex_columns.h +++ b/include/neug/execution/common/columns/vertex_columns.h @@ -31,8 +31,7 @@ class IVertexColumn : public IContextColumn { IVertexColumn() : type_(DataType(DataTypeId::kVertex)) {} virtual ~IVertexColumn() = default; - __attribute__((always_inline)) ContextColumnType column_type() - const override { + ContextColumnType column_type() const override { return ContextColumnType::kVertex; } @@ -89,8 +88,7 @@ class SLVertexColumn : public IVertexColumn { std::to_string(size()) + "]"; } - __attribute__((always_inline)) VertexColumnType vertex_column_type() - const override { + VertexColumnType vertex_column_type() const override { return VertexColumnType::kSingle; } @@ -116,7 +114,7 @@ class SLVertexColumn : public IVertexColumn { std::shared_ptr union_col( std::shared_ptr other) const override; - void generate_dedup_offset(std::vector& offsets) const override; + bool generate_dedup_offset(std::vector& offsets) const override; std::pair, std::vector>> generate_aggregate_offset() const override; @@ -175,8 +173,7 @@ class MSVertexColumn : public IVertexColumn { std::to_string(size()) + "]"; } - __attribute__((always_inline)) VertexColumnType vertex_column_type() - const override { + VertexColumnType vertex_column_type() const override { return VertexColumnType::kMultiSegment; } @@ -234,6 +231,8 @@ class MSVertexColumn : public IVertexColumn { return vertices_[seg_id].second; } + bool generate_dedup_offset(std::vector& offsets) const override; + private: friend class MSVertexColumnBuilder; std::vector>> vertices_; @@ -320,8 +319,7 @@ class MLVertexColumn : public IVertexColumn { std::to_string(size()) + "]"; } - __attribute__((always_inline)) VertexColumnType vertex_column_type() - const override { + VertexColumnType vertex_column_type() const override { return VertexColumnType::kMultiple; } @@ -353,7 +351,7 @@ class MLVertexColumn : public IVertexColumn { std::set get_labels_set() const override { return labels_; } - void generate_dedup_offset(std::vector& offsets) const override; + bool generate_dedup_offset(std::vector& offsets) const override; private: friend class MLVertexColumnBuilder; diff --git a/src/execution/common/columns/arrow_context_column.cc b/src/execution/common/columns/arrow_context_column.cc index 7c666504..5f0647b7 100644 --- a/src/execution/common/columns/arrow_context_column.cc +++ b/src/execution/common/columns/arrow_context_column.cc @@ -238,105 +238,6 @@ shuffle_impl( return result_array; } -template -static bool less_than(const std::vector>& columns, - size_t size, size_t offset_a, size_t offset_b) { - auto [array_idx_a, local_offset_a] = - locate_array_and_offset(columns, size, offset_a); - auto [array_idx_b, local_offset_b] = - locate_array_and_offset(columns, size, offset_b); - auto casted_a = - std::static_pointer_cast(columns[array_idx_a]); - auto casted_b = - std::static_pointer_cast(columns[array_idx_b]); - return casted_a->Value(local_offset_a) < casted_b->Value(local_offset_b); -} - -template -static bool equal(const std::vector>& columns, - size_t size, size_t offset_a, size_t offset_b) { - auto [array_idx_a, local_offset_a] = - locate_array_and_offset(columns, size, offset_a); - auto [array_idx_b, local_offset_b] = - locate_array_and_offset(columns, size, offset_b); - auto casted_a = - std::static_pointer_cast(columns[array_idx_a]); - auto casted_b = - std::static_pointer_cast(columns[array_idx_b]); - return casted_a->Value(local_offset_a) == casted_b->Value(local_offset_b); -} - -// Template function to generate dedup offsets -template -static void generate_dedup_offset( - const std::vector>& columns, size_t size, - std::vector& offsets) { - // Create vector of all offsets - std::vector row_indices(size); - std::iota(row_indices.begin(), row_indices.end(), 0); - - // Create comparator that directly uses Arrow arrays - auto compare = [&](size_t a, size_t b) -> bool { - if (equal(columns, size, a, b)) { - return a < b; - } - return less_than(columns, size, a, b); - }; - - // Sort indices using stable_sort to maintain order for equal values - std::stable_sort(row_indices.begin(), row_indices.end(), compare); - - // Extract deduplicated offsets - offsets.clear(); - if (row_indices.empty()) { - return; - } - - offsets.push_back(row_indices[0]); - - for (size_t i = 1; i < row_indices.size(); ++i) { - if (!equal(columns, size, row_indices[i], - row_indices[i - 1])) { - offsets.push_back(row_indices[i]); - } - } -} - -// Define DISPATCH macro to generate dedup offsets based on Arrow type -void dispatch_generate_dedup_offset( - const std::vector>& columns, size_t size, - const std::shared_ptr& arrow_type, - std::vector& offsets) { - // Use Arrow type ID for dispatch - switch (arrow_type->id()) { -#define ARROW_TYPE_DISPATCHER_DEDUP(arrow_type_id, arrow_array_type) \ - case arrow::Type::arrow_type_id: { \ - generate_dedup_offset(columns, size, offsets); \ - break; \ - } - - ARROW_TYPE_DISPATCHER_DEDUP(BOOL, arrow::BooleanArray) - ARROW_TYPE_DISPATCHER_DEDUP(INT64, arrow::Int64Array) - ARROW_TYPE_DISPATCHER_DEDUP(INT32, arrow::Int32Array) - ARROW_TYPE_DISPATCHER_DEDUP(UINT32, arrow::UInt32Array) - ARROW_TYPE_DISPATCHER_DEDUP(UINT64, arrow::UInt64Array) - ARROW_TYPE_DISPATCHER_DEDUP(FLOAT, arrow::FloatArray) - ARROW_TYPE_DISPATCHER_DEDUP(DOUBLE, arrow::DoubleArray) - ARROW_TYPE_DISPATCHER_DEDUP(STRING, arrow::StringArray) - ARROW_TYPE_DISPATCHER_DEDUP(LARGE_STRING, arrow::LargeStringArray) - ARROW_TYPE_DISPATCHER_DEDUP(DATE32, arrow::Date32Array) - ARROW_TYPE_DISPATCHER_DEDUP(DATE64, arrow::Date64Array) - ARROW_TYPE_DISPATCHER_DEDUP(TIMESTAMP, arrow::TimestampArray) - // Interval type has been converted to arrow string type - -#undef ARROW_TYPE_DISPATCHER_DEDUP - - default: - THROW_NOT_SUPPORTED_EXCEPTION("Unsupported arrow type for dedup: " + - arrow_type->ToString()); - } -} - std::shared_ptr ArrowArrayContextColumnBuilder::finish() { return std::make_shared(columns_); } @@ -467,17 +368,6 @@ Value ArrowArrayContextColumn::get_elem(size_t idx) const { } } -void ArrowArrayContextColumn::generate_dedup_offset( - std::vector& offsets) const { - if (columns_.empty()) { - offsets.clear(); - return; - } - - auto arrow_type = columns_[0]->type(); - dispatch_generate_dedup_offset(columns_, size_, arrow_type, offsets); -} - std::shared_ptr ArrowArrayContextColumn::cast_to_value_column() const { auto builder = ColumnsUtils::create_builder(elem_type()); diff --git a/src/execution/common/columns/struct_columns.cc b/src/execution/common/columns/struct_columns.cc index a0408820..d5956b94 100644 --- a/src/execution/common/columns/struct_columns.cc +++ b/src/execution/common/columns/struct_columns.cc @@ -71,10 +71,6 @@ Value StructColumn::get_elem(size_t idx) const { return Value::STRUCT(type_, std::move(struct_values)); } -void StructColumn::generate_dedup_offset(std::vector& offsets) const { - LOG(FATAL) << "not implemented for " << this->column_info(); -} - StructColumnBuilder::StructColumnBuilder(DataType type) : type_(type) { const auto& child_types = StructType::GetChildTypes(type); for (const auto& child_type : child_types) { diff --git a/src/execution/common/columns/vertex_columns.cc b/src/execution/common/columns/vertex_columns.cc index 135e0bb3..89b880a5 100644 --- a/src/execution/common/columns/vertex_columns.cc +++ b/src/execution/common/columns/vertex_columns.cc @@ -59,7 +59,7 @@ std::shared_ptr SLVertexColumn::optional_shuffle( return builder.finish(); } -void SLVertexColumn::generate_dedup_offset(std::vector& offsets) const { +bool SLVertexColumn::generate_dedup_offset(std::vector& offsets) const { offsets.clear(); std::vector bitset; @@ -92,6 +92,7 @@ void SLVertexColumn::generate_dedup_offset(std::vector& offsets) const { if (flag) { offsets.push_back(idx); } + return true; } std::pair, std::vector>> @@ -225,6 +226,25 @@ std::shared_ptr MSVertexColumnBuilder::finish() { } } +bool MSVertexColumn::generate_dedup_offset(std::vector& offsets) const { + offsets.clear(); + std::set vset; + bool null_seen = false; + size_t len = size(); + for (size_t i = 0; i != len; ++i) { + auto cur = get_vertex(i); + if (cur.vid_ == std::numeric_limits::max()) { + if (!null_seen) { + null_seen = true; + offsets.push_back(i); + } + } else if (vset.find(cur) == vset.end()) { + offsets.push_back(i); + vset.insert(cur); + } + } + return true; +} std::shared_ptr MLVertexColumn::shuffle( const std::vector& offsets) const { MLVertexColumnBuilderOpt builder(this->get_labels_set()); @@ -259,7 +279,7 @@ std::shared_ptr MLVertexColumn::optional_shuffle( return builder.finish(); } -void MLVertexColumn::generate_dedup_offset(std::vector& offsets) const { +bool MLVertexColumn::generate_dedup_offset(std::vector& offsets) const { offsets.clear(); std::set vset; size_t n = vertices_.size(); @@ -270,6 +290,7 @@ void MLVertexColumn::generate_dedup_offset(std::vector& offsets) const { vset.insert(cur); } } + return true; } std::shared_ptr MLVertexColumnBuilder::finish() { diff --git a/src/execution/common/operators/retrieve/dedup.cc b/src/execution/common/operators/retrieve/dedup.cc index 11960f0f..202c696f 100644 --- a/src/execution/common/operators/retrieve/dedup.cc +++ b/src/execution/common/operators/retrieve/dedup.cc @@ -33,9 +33,10 @@ neug::result Dedup::dedup(Context&& ctx, std::vector offsets; if (cols.size() == 0) { return ctx; - } else if (cols.size() == 1) { - ctx.get(cols[0])->generate_dedup_offset(offsets); + } + if (cols.size() == 1 && ctx.get(cols[0])->generate_dedup_offset(offsets)) { } else { + offsets.clear(); phmap::flat_hash_set set; for (size_t r_i = 0; r_i < row_num; ++r_i) { std::vector bytes; diff --git a/src/execution/common/operators/retrieve/unfold.cc b/src/execution/common/operators/retrieve/unfold.cc index 4109a917..33e03820 100644 --- a/src/execution/common/operators/retrieve/unfold.cc +++ b/src/execution/common/operators/retrieve/unfold.cc @@ -30,7 +30,7 @@ neug::result Unfold::unfold(Context&& ctxs, int key, int alias) { LOG(ERROR) << "Unfold column type is not list"; RETURN_INVALID_ARGUMENT_ERROR("Unfold column type is not list"); } - auto list_col = std::dynamic_pointer_cast(col); + auto list_col = std::dynamic_pointer_cast(col); auto [ptr, offsets] = list_col->unfold(); ctxs.set_with_reshuffle(alias, ptr, offsets); From 99d150b10a0eb425b7a6257dec38a7a7dfb423ba Mon Sep 17 00:00:00 2001 From: liulx20 Date: Thu, 19 Mar 2026 13:45:35 +0800 Subject: [PATCH 47/60] Correct the is_optional interface behavior for certain columns (#90) --- include/neug/execution/common/columns/arrow_context_column.h | 5 +++++ include/neug/execution/common/columns/edge_columns.h | 4 ++++ include/neug/execution/common/columns/i_context_column.h | 2 +- include/neug/execution/common/columns/list_columns.h | 2 ++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/include/neug/execution/common/columns/arrow_context_column.h b/include/neug/execution/common/columns/arrow_context_column.h index 32c8ba89..2c619740 100644 --- a/include/neug/execution/common/columns/arrow_context_column.h +++ b/include/neug/execution/common/columns/arrow_context_column.h @@ -155,6 +155,11 @@ class ArrowStreamContextColumn : public IContextColumn { return Value(DataType::SQLNULL); } + bool is_optional() const override { + LOG(FATAL) << "is_optional not implemented for arrow stream column"; + return false; + } + private: std::shared_ptr first_batch_; std::vector> suppliers_; diff --git a/include/neug/execution/common/columns/edge_columns.h b/include/neug/execution/common/columns/edge_columns.h index 75f9f653..af3632d9 100644 --- a/include/neug/execution/common/columns/edge_columns.h +++ b/include/neug/execution/common/columns/edge_columns.h @@ -501,6 +501,8 @@ class SDMLEdgeColumn : public IEdgeColumn { return std::get<1>(tup) != std::numeric_limits::max(); } + bool is_optional() const override { return is_optional_; } + private: friend class SDMLEdgeColumnBuilder; Direction dir_; @@ -621,6 +623,8 @@ class BDMLEdgeColumn : public IEdgeColumn { return std::get<1>(tup) != std::numeric_limits::max(); } + bool is_optional() const override { return is_optional_; } + private: friend class BDMLEdgeColumnBuilder; std::map index_; diff --git a/include/neug/execution/common/columns/i_context_column.h b/include/neug/execution/common/columns/i_context_column.h index adcfcd33..4c06b41f 100644 --- a/include/neug/execution/common/columns/i_context_column.h +++ b/include/neug/execution/common/columns/i_context_column.h @@ -74,7 +74,7 @@ class IContextColumn { virtual Value get_elem(size_t idx) const = 0; virtual bool has_value(size_t idx) const { return true; } - virtual bool is_optional() const { return false; } + virtual bool is_optional() const = 0; virtual bool generate_dedup_offset(std::vector& offsets) const { LOG(ERROR) << "generate_dedup_offset not implemented for " diff --git a/include/neug/execution/common/columns/list_columns.h b/include/neug/execution/common/columns/list_columns.h index 6d9c2bc1..743016fc 100644 --- a/include/neug/execution/common/columns/list_columns.h +++ b/include/neug/execution/common/columns/list_columns.h @@ -86,6 +86,8 @@ class ListColumn : public IContextColumn { return ptr; } + bool is_optional() const override { return false; } + private: template std::pair, std::vector> unfold_impl() From 3650f3ef7dfd8b4fadff83f3bf7bbb008b8271f1 Mon Sep 17 00:00:00 2001 From: BingqingLyu Date: Thu, 19 Mar 2026 13:52:46 +0800 Subject: [PATCH 48/60] add a codegraph example (#87) Co-authored-by: Longbin Lai --- doc/source/index.rst | 1 + .../tutorials/codegraph-openclaw-example.md | 353 ++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 doc/source/tutorials/codegraph-openclaw-example.md diff --git a/doc/source/index.rst b/doc/source/index.rst index 4dfcc1fb..dbb5d3e5 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -34,6 +34,7 @@ NeuG documentation :caption: Tutorials tutorials/tinysnb_tutorial + tutorials/codegraph-openclaw-example .. toctree:: :maxdepth: 1 diff --git a/doc/source/tutorials/codegraph-openclaw-example.md b/doc/source/tutorials/codegraph-openclaw-example.md new file mode 100644 index 00000000..d9621de7 --- /dev/null +++ b/doc/source/tutorials/codegraph-openclaw-example.md @@ -0,0 +1,353 @@ +# CodeGraph: Code Analysis with Knowledge Graph + +## Introduction + +CodeGraph is a code analysis Skill built on **NeuG** (graph database) and **zvec** (vector database). It indexes source code into a knowledge graph containing nodes (File, Function, Class, Module, Commit) and edges (CALLS, IMPORTS, INHERITS, MODIFIES, etc.), plus semantic embeddings for each function. + +This combination enables analyses that grep, LSP, or pure vector search cannot accomplish alone. + +### Core Capabilities + + +| Capability | Description | +| ----------------------- | ------------------------------------------------------- | +| Call Chain Analysis | Find callers, callees, N-hop impact analysis | +| Architecture Analysis | Auto-discover layers, bridge functions, module coupling | +| Dead Code Detection | Identify functions with zero callers | +| Semantic Search | Find functions by natural language description | +| Hotspot Analysis | Identify high-risk functions (fan-in × fan-out) | +| Evolution Analysis | Track commit history, function modification records | +| Bug Root Cause Analysis | Map GitHub issues to code locations | + +## Example: Analyzing OpenClaw Codebase + +This section demonstrates CodeGraph usage with the OpenClaw codebase as an example. + +### Prerequisites + +CodeGraph requires Python 3.10+ and PyTorch 2.4+. + +```bash +# Create virtual environment +cd /path/to/your/project +python3 -m venv .venv + +# Activate and install codegraph-ai +source .venv/bin/activate +pip install codegraph-ai +``` + +### Environment Setup + +```bash +# Point to the database directory +export CODESCOPE_DB_DIR="/path/to/your/project/.codegraph" + +# If you have offline mode for HuggingFace, you can use the offline mode +export HF_HUB_OFFLINE="1" +``` + +### Indexing the Codebase + +```bash +# Create index (first time) +codegraph init --repo /path/to/your/project --lang auto --commits 100 + +# Check index status +codegraph status --db $CODESCOPE_DB_DIR +``` + +**Actual output from OpenClaw:** + +``` +============================================================ +CodeScope Index Status: /path/to/openclaw/.codegraph +============================================================ + +Graph: + File : 12,857 + Function : 24,173 + Class : 255 + Module : 380 + Commit : 100 + +Edges: + CALLS : 41,269 + TOUCHES : 605 + MODIFIES : 0 + +Vectors: 24,173 function embeddings +============================================================ +``` + +--- + +## CLI Usage Examples + +### 1. Check Status + +```bash +codegraph status --db $CODESCOPE_DB_DIR 2>/dev/null +``` + +### 2. Natural Language Query + +```bash +codegraph query "Who calls runHeartbeatOnce?" --db $CODESCOPE_DB_DIR 2>/dev/null +``` + +**Actual output:** + +``` +Question type: structural +Retrieved 6 evidence items in 74ms: + +[1] (caller) startGatewayServer (src/gateway/server.impl.ts) — hop=2 +[2] (caller) createGatewayReloadHandlers (src/gateway/server-reload-handlers.ts) — hop=2 +[3] (caller) executeJob (src/cron/service/timer.ts) — hop=2 +[4] (caller) executeJobCore (src/cron/service/timer.ts) — hop=1 +[5] (caller) buildGatewayCronService (src/gateway/server-cron.ts) — hop=1 +[6] (caller) executeJobCoreWithTimeout (src/cron/service/timer.ts) — hop=2 +``` + +### 3. Generate Architecture Report + +```bash +codegraph analyze --db $CODESCOPE_DB_DIR --output architecture-report.md 2>/dev/null +``` + +**Report includes:** + +- Codebase overview (files, functions, call edges, classes, modules) +- Subsystem distribution +- Architectural layers (with Mermaid diagrams) +- Bridge functions +- Hotspots +- Module coupling +- Dead code density + +--- + +## Python API Examples + +For complex queries, use the Python API: + +### Setup + +```python +import os +os.environ['HF_HUB_OFFLINE'] = '1' + +from codegraph.core import CodeScope +cs = CodeScope(os.environ['CODESCOPE_DB_DIR']) +``` + +### Find Callers of a Function + +```python +rows = list(cs.conn.execute(''' + MATCH (caller:Function)-[:CALLS]->(f:Function {name: "runHeartbeatOnce"}) + RETURN caller.name, caller.file_path +''')) +for r in rows: + print(f"{r[0]} @ {r[1]}") +``` + +**Actual output:** + +``` +executeJobCore @ src/cron/service/timer.ts +buildGatewayCronService @ src/gateway/server-cron.ts +``` + +### Find Functions Called by a Function + +```python +rows = list(cs.conn.execute(''' + MATCH (f:Function {name: "runHeartbeatOnce"})-[:CALLS]->(callee:Function) + RETURN callee.name + LIMIT 20 +''')) +for r in rows: + print(f"-> {r[0]}") +``` + +**Actual output:** + +``` +-> parseAgentSessionKey +-> resolveDefaultAgentId +-> resolveHeartbeatConfig +-> areHeartbeatsEnabled +-> isHeartbeatEnabledForAgent +-> resolveHeartbeatIntervalMs +-> nowMs +-> now +-> isWithinActiveHours +-> resolveHeartbeatPreflight +-> emitHeartbeatEvent +-> resolveCronSession +-> saveSessionStore +-> resolveHeartbeatDeliveryTarget +-> resolveHeartbeatVisibility +-> resolveHeartbeatSenderContext +-> resolveEffectiveMessagesConfig +-> resolveAgentWorkspaceDir +-> resolveHeartbeatRunPrompt +-> appendCronStyleCurrentTimeLine +``` + +### Impact Analysis (N-hop Callers) + +```python +rows = list(cs.conn.execute(''' + MATCH (caller:Function)-[:CALLS*1..2]->(f:Function {name: "runHeartbeatOnce"}) + RETURN DISTINCT caller.name, caller.file_path + LIMIT 20 +''')) +for r in rows: + print(f"{r[0]} @ {r[1]}") +``` + +**Actual output:** + +``` +executeJobCore @ src/cron/service/timer.ts +buildGatewayCronService @ src/gateway/server-cron.ts +executeJobCoreWithTimeout @ src/cron/service/timer.ts +executeJob @ src/cron/service/timer.ts +createGatewayReloadHandlers @ src/gateway/server-reload-handlers.ts +startGatewayServer @ src/gateway/server.impl.ts +``` + +--- + +## Built-in Analysis Methods + +### Hotspots + +High-risk functions ranked by fan-in × fan-out: + +```python +for h in cs.hotspots(topk=10): + print(f"{h.name} @ {h.file_path}") + print(f" fan_in={h.fan_in}, fan_out={h.fan_out}") +``` + +**Actual output:** + +``` +push @ ui/src/ui/chat/input-history.ts + fan_in=1747, fan_out=0 +createConfigIO @ src/config/io.ts + fan_in=18, fan_out=57 +fn @ extensions/diffs/assets/viewer-runtime.js + fan_in=533, fan_out=1 +runEmbeddedPiAgent @ src/agents/pi-embedded-runner/run.ts + fan_in=14, fan_out=65 +startGatewayServer @ src/gateway/server.impl.ts + fan_in=10, fan_out=88 +now @ src/auto-reply/reply/export-html/template.security.test.ts + fan_in=857, fan_out=0 +loadOpenClawPlugins @ src/plugins/loader.ts + fan_in=21, fan_out=36 +runCronIsolatedAgentTurn @ src/cron/isolated-agent/run.ts + fan_in=11, fan_out=56 +loadSessionStore @ src/config/sessions/store.ts + fan_in=60, fan_out=8 +getReplyFromConfig @ src/auto-reply/reply/get-reply.ts + fan_in=20, fan_out=24 +``` + +### Bridge Functions + +Functions called from many distinct modules (cross-subsystem connectors): + +```python +for b in cs.bridge_functions(topk=10): + print(f"{b.name} @ {b.file_path}") + print(f" modules={b.module_count}") +``` + +**Actual output:** + +``` +push @ ui/src/ui/chat/input-history.ts + modules=167 +now @ src/auto-reply/reply/export-html/template.security.test.ts + modules=135 +fn @ extensions/diffs/assets/viewer-runtime.js + modules=103 +error @ src/plugins/config-schema.ts + modules=102 +toString @ extensions/discord/src/send.types.ts + modules=95 +next @ src/wizard/session.ts + modules=36 +shouldLogVerbose @ src/globals.ts + modules=33 +release @ src/browser/cdp-proxy-bypass.ts + modules=32 +formatCliCommand @ src/cli/command-format.ts + modules=31 +isDirectory @ src/infra/path-env.ts + modules=28 +``` + +### Dead Code Detection + +Functions with zero callers: + +```python +for d in cs.dead_code()[:10]: + print(f"{d.name} @ {d.file_path}") +``` + +**Actual output:** + +``` +promptUrlWidgetExtension @ .pi/extensions/prompt-url-widget.ts +showPagedSelectList @ .pi/extensions/ui/paged-select.ts +copyToClipboard @ .venv/lib/python3.10/site-packages/sklearn/utils/_repr_html/estimator.js +CodeSection @ .venv/lib/python3.10/site-packages/torch/utils/model_dump/code.js +ExtraJsonSection @ .venv/lib/python3.10/site-packages/torch/utils/model_dump/code.js +... +``` + +> **Note**: Dead code detection may include external dependencies. Filter by `is_external = 0` for project-specific results. + +### Semantic Search + +Find functions by natural language description: + +```python +results = cs.vector_only_search('heartbeat periodic wake agent schedule', topk=5) +for r in results: + print(f"id={r['id'][:20]}... score={r['score']:.3f}") +``` + +**Actual output:** + +``` +id=59744ec14e23575012c1... score=0.514 +id=0b27570192377b7077cd... score=0.481 +id=11fad68a6ba0d7fa0228... score=0.478 +id=b33f6f3241c0a61d7118... score=0.477 +id=8221fa3eb46b7e06e561... score=0.473 +``` + +--- + +## Cypher Query Templates + +The following templates serve as reference queries for analyzing codebases. Replace `FUNC_NAME`, `PATH`, `MODULE` with your specific values: + +| Analysis | Cypher Query | +| --------------------- | ------------------------------------------------------------------------------------------------------------- | +| Find callers | `MATCH (c:Function)-[:CALLS]->(f:Function {name: "FUNC_NAME"}) RETURN c.name, c.file_path` | +| Find callees | `MATCH (f:Function {name: "FUNC_NAME"})-[:CALLS]->(c:Function) RETURN c.name` | +| Impact (N hops) | `MATCH (c:Function)-[:CALLS*1..N]->(f:Function {name: "FUNC_NAME"}) RETURN DISTINCT c.name` | +| Functions in file | `MATCH (file:File)-[:DEFINES_FUNC]->(f:Function) WHERE file.path CONTAINS "PATH" RETURN f.name` | +| Files in module | `MATCH (f:File)-[:BELONGS_TO]->(m:Module) WHERE m.name = "MODULE" RETURN f.path` | +| Class hierarchy | `MATCH (c:Class)-[:INHERITS]->(p:Class) RETURN c.name, p.name` | +| Most-called functions | `MATCH (f:Function)<-[:CALLS]-(c:Function) RETURN f.name, count(c) as callers ORDER BY callers DESC LIMIT 10` | From 3b34765ee23222dc8156383e33efc3add94223d7 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 19 Mar 2026 13:55:55 +0800 Subject: [PATCH 49/60] add checkRowIndex --- .../neug/driver/internal/InternalResultSet.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index 0258f7db..21476a9b 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -99,6 +99,7 @@ public Object getObject(String columnName) { @Override public Object getObject(int columnIndex) { // Return the appropriate type based on the array type + checkRowIndex(); checkIndex(columnIndex); Results.Array array = response.getArrays(columnIndex); try { @@ -340,6 +341,7 @@ public int getInt(String columnName) { @Override public int getInt(int columnIndex) { + checkRowIndex(); checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (arr.hasInt32Array()) { @@ -363,6 +365,7 @@ public long getLong(String columnName) { @Override public long getLong(int columnIndex) { + checkRowIndex(); checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (arr.hasInt64Array()) { @@ -386,6 +389,7 @@ public String getString(String columnName) { @Override public String getString(int columnIndex) { + checkRowIndex(); checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (!arr.hasStringArray()) { @@ -408,6 +412,7 @@ public Date getDate(String columnName) { @Override public Date getDate(int columnIndex) { + checkRowIndex(); checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (!arr.hasDateArray()) { @@ -430,6 +435,7 @@ public Timestamp getTimestamp(String columnName) { @Override public Timestamp getTimestamp(int columnIndex) { + checkRowIndex(); checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (!arr.hasTimestampArray()) { @@ -452,6 +458,7 @@ public boolean getBoolean(String columnName) { @Override public boolean getBoolean(int columnIndex) { + checkRowIndex(); checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (!arr.hasBoolArray()) { @@ -474,6 +481,7 @@ public double getDouble(String columnName) { @Override public double getDouble(int columnIndex) { + checkRowIndex(); checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (arr.hasFloatArray()) { @@ -497,6 +505,7 @@ public float getFloat(String columnName) { @Override public float getFloat(int columnIndex) { + checkRowIndex(); checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); if (arr.hasFloatArray()) { @@ -520,6 +529,7 @@ public BigDecimal getBigDecimal(String columnName) { @Override public BigDecimal getBigDecimal(int columnIndex) { + checkRowIndex(); checkIndex(columnIndex); Results.Array arr = response.getArrays(columnIndex); Number value = getNumericValue(arr); @@ -576,6 +586,12 @@ private void checkIndex(int columnIndex) { } } + private void checkRowIndex() { + if (currentIndex < 0 || currentIndex >= response.getRowCount()) { + throw new IndexOutOfBoundsException("Cursor is not positioned on a valid row"); + } + } + /** * Generic method to extract numeric value from any numeric array type. * From 6c88975b52a5bc8bc00582a930377d51a3565b2d Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 19 Mar 2026 14:15:58 +0800 Subject: [PATCH 50/60] add update_was_null --- .../driver/internal/InternalResultSet.java | 152 +++++------------- 1 file changed, 36 insertions(+), 116 deletions(-) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index 21476a9b..baa0ef24 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -109,6 +109,12 @@ public Object getObject(int columnIndex) { } } + private void update_was_null(ByteString nullBitmap) { + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; + } + private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyHandled) throws Exception { switch (array.getTypedArrayCase()) { @@ -116,10 +122,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH { if (!nullAlreadyHandled) { ByteString nullBitmap = array.getStringArray().getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } return array.getStringArray().getValues(rowIndex); } @@ -127,10 +130,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH { if (!nullAlreadyHandled) { ByteString nullBitmap = array.getInt32Array().getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } return array.getInt32Array().getValues(rowIndex); } @@ -138,10 +138,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH { if (!nullAlreadyHandled) { ByteString nullBitmap = array.getInt64Array().getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } return array.getInt64Array().getValues(rowIndex); } @@ -149,10 +146,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH { if (!nullAlreadyHandled) { ByteString nullBitmap = array.getBoolArray().getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } return array.getBoolArray().getValues(rowIndex); } @@ -160,10 +154,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH { if (!nullAlreadyHandled) { ByteString nullBitmap = array.getFloatArray().getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } return array.getFloatArray().getValues(rowIndex); } @@ -171,10 +162,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH { if (!nullAlreadyHandled) { ByteString nullBitmap = array.getDoubleArray().getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } return array.getDoubleArray().getValues(rowIndex); } @@ -182,10 +170,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH { if (!nullAlreadyHandled) { ByteString nullBitmap = array.getTimestampArray().getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } return array.getTimestampArray().getValues(rowIndex); } @@ -193,10 +178,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH { if (!nullAlreadyHandled) { ByteString nullBitmap = array.getDateArray().getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } return array.getDateArray().getValues(rowIndex); } @@ -206,10 +188,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH if (!nullAlreadyHandled) { ByteString nullBitmap = listArray.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } int start = listArray.getOffsets(rowIndex); @@ -226,10 +205,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH if (!nullAlreadyHandled) { ByteString nullBitmap = structArray.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } List struct = new ArrayList<>(structArray.getFieldsCount()); for (int i = 0; i < structArray.getFieldsCount(); i++) { @@ -243,10 +219,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH ObjectMapper mapper = JsonUtil.getInstance(); if (!nullAlreadyHandled) { ByteString nullBitmap = vertexArray.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } Map map = mapper.readValue( @@ -260,10 +233,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH ObjectMapper mapper = JsonUtil.getInstance(); if (!nullAlreadyHandled) { ByteString nullBitmap = edgeArray.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } Map map = mapper.readValue( @@ -277,10 +247,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH ObjectMapper mapper = JsonUtil.getInstance(); if (!nullAlreadyHandled) { ByteString nullBitmap = pathArray.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } Map map = mapper.readValue( @@ -293,10 +260,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH Results.IntervalArray intervalArray = array.getIntervalArray(); if (!nullAlreadyHandled) { ByteString nullBitmap = intervalArray.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } return intervalArray.getValues(rowIndex); } @@ -305,10 +269,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH Results.UInt32Array uint32Array = array.getUint32Array(); if (!nullAlreadyHandled) { ByteString nullBitmap = uint32Array.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } // Convert uint32 to long to avoid overflow return Integer.toUnsignedLong(uint32Array.getValues(rowIndex)); @@ -318,10 +279,7 @@ private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyH Results.UInt64Array uint64Array = array.getUint64Array(); if (!nullAlreadyHandled) { ByteString nullBitmap = uint64Array.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) - == 0; + update_was_null(nullBitmap); } // Convert uint64 to BigInteger to avoid overflow long value = uint64Array.getValues(rowIndex); @@ -348,10 +306,7 @@ public int getInt(int columnIndex) { Results.Int32Array array = arr.getInt32Array(); ByteString nullBitmap = array.getValidity(); int value = array.getValues(currentIndex); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) - == 0; + update_was_null(nullBitmap); return value; } return getNumericValue(arr).intValue(); @@ -372,10 +327,7 @@ public long getLong(int columnIndex) { Results.Int64Array array = arr.getInt64Array(); ByteString nullBitmap = array.getValidity(); long value = array.getValues(currentIndex); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) - == 0; + update_was_null(nullBitmap); return value; } return getNumericValue(arr).longValue(); @@ -398,9 +350,7 @@ public String getString(int columnIndex) { Results.StringArray array = arr.getStringArray(); ByteString nullBitmap = array.getValidity(); String value = array.getValues(currentIndex); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + update_was_null(nullBitmap); return value; } @@ -421,9 +371,7 @@ public Date getDate(int columnIndex) { Results.DateArray array = arr.getDateArray(); ByteString nullBitmap = array.getValidity(); long timestamp = array.getValues(currentIndex); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + update_was_null(nullBitmap); return new Date(timestamp); } @@ -444,9 +392,7 @@ public Timestamp getTimestamp(int columnIndex) { Results.TimestampArray array = arr.getTimestampArray(); ByteString nullBitmap = array.getValidity(); long timestamp = array.getValues(currentIndex); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + update_was_null(nullBitmap); return new Timestamp(timestamp); } @@ -467,9 +413,7 @@ public boolean getBoolean(int columnIndex) { Results.BoolArray array = arr.getBoolArray(); ByteString nullBitmap = array.getValidity(); boolean value = array.getValues(currentIndex); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + update_was_null(nullBitmap); return value; } @@ -488,10 +432,7 @@ public double getDouble(int columnIndex) { Results.FloatArray array = arr.getFloatArray(); ByteString nullBitmap = array.getValidity(); float value = array.getValues(currentIndex); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) - == 0; + update_was_null(nullBitmap); return value; } return getNumericValue(arr).doubleValue(); @@ -512,10 +453,7 @@ public float getFloat(int columnIndex) { Results.FloatArray array = arr.getFloatArray(); ByteString nullBitmap = array.getValidity(); float value = array.getValues(currentIndex); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) - == 0; + update_was_null(nullBitmap); return value; } return getNumericValue(arr).floatValue(); @@ -604,50 +542,35 @@ private Number getNumericValue(Results.Array arr) { if (arr.hasInt32Array()) { Results.Int32Array array = arr.getInt32Array(); nullBitmap = array.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) - == 0; + update_was_null(nullBitmap); return array.getValues(currentIndex); } if (arr.hasInt64Array()) { Results.Int64Array array = arr.getInt64Array(); nullBitmap = array.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) - == 0; + update_was_null(nullBitmap); return array.getValues(currentIndex); } if (arr.hasFloatArray()) { Results.FloatArray array = arr.getFloatArray(); nullBitmap = array.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) - == 0; + update_was_null(nullBitmap); return array.getValues(currentIndex); } if (arr.hasDoubleArray()) { Results.DoubleArray array = arr.getDoubleArray(); nullBitmap = array.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) - == 0; + update_was_null(nullBitmap); return array.getValues(currentIndex); } if (arr.hasUint32Array()) { Results.UInt32Array array = arr.getUint32Array(); nullBitmap = array.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) - == 0; + update_was_null(nullBitmap); // Convert unsigned int32 to signed long to avoid overflow return Integer.toUnsignedLong(array.getValues(currentIndex)); } @@ -655,10 +578,7 @@ private Number getNumericValue(Results.Array arr) { if (arr.hasUint64Array()) { Results.UInt64Array array = arr.getUint64Array(); nullBitmap = array.getValidity(); - was_null = - !nullBitmap.isEmpty() - && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) - == 0; + update_was_null(nullBitmap); // Convert unsigned int64 to BigInteger to avoid overflow long value = array.getValues(currentIndex); return new BigInteger(Long.toUnsignedString(value)); From a3dd71bc6018eadcee27fc8ea0a2ee4b44b30ab2 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 19 Mar 2026 14:30:28 +0800 Subject: [PATCH 51/60] update doc --- doc/source/reference/java_api/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/reference/java_api/index.md b/doc/source/reference/java_api/index.md index 89ee3b6e..d1dabfff 100644 --- a/doc/source/reference/java_api/index.md +++ b/doc/source/reference/java_api/index.md @@ -94,7 +94,7 @@ You can use either the C++ binary or the Python API to start the server. From the repository root: ```bash -cmake -S . -B build +cmake -S . -B build -DBUILD_EXECUTABLES=ON -DBUILD_HTTP_SERVER=ON cmake --build build --target rt_server -j ``` From eef7ce0e7b51e3d0d046c00466e69b24e794b3f6 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 19 Mar 2026 15:09:46 +0800 Subject: [PATCH 52/60] fix --- .../com/alibaba/neug/driver/internal/InternalResultSet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index baa0ef24..78eed706 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -112,7 +112,7 @@ public Object getObject(int columnIndex) { private void update_was_null(ByteString nullBitmap) { was_null = !nullBitmap.isEmpty() - && (nullBitmap.byteAt(rowIndex / 8) & (1 << (rowIndex % 8))) == 0; + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; } private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyHandled) From dabb89459aaabeff1afb8d31e70b90da3339b58e Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 19 Mar 2026 19:22:45 +0800 Subject: [PATCH 53/60] update doc --- doc/source/_scripts/generate_python_docs.py | 4 +- doc/source/conf.py | 5 +- doc/source/development/code_style_guide.md | 26 +++ doc/source/reference/_meta.ts | 2 +- doc/source/reference/cpp_api/connection.md | 2 +- doc/source/reference/cpp_api/neug_db.md | 7 + doc/source/reference/cpp_api/query_result.md | 177 ++---------------- doc/source/reference/cpp_api/service.md | 2 +- doc/source/reference/java_api/index.md | 10 +- doc/source/reference/python_api/connection.md | 4 +- .../reference/python_api/query_result.md | 16 +- doc/source/reference/python_api/session.md | 2 +- include/neug/main/query_result.h | 24 ++- .../com/alibaba/neug/driver/ResultSet.java | 8 - .../driver/internal/InternalResultSet.java | 10 - .../neug/driver/InternalResultSetTest.java | 9 - 16 files changed, 87 insertions(+), 221 deletions(-) diff --git a/doc/source/_scripts/generate_python_docs.py b/doc/source/_scripts/generate_python_docs.py index 6149e5a2..8064e35f 100644 --- a/doc/source/_scripts/generate_python_docs.py +++ b/doc/source/_scripts/generate_python_docs.py @@ -96,7 +96,7 @@ def clean_generated_docs(output_dir: Path, scripts_dir: Path): def generate_docs(): """Generate documentation for all modules using pydoc-markdown.""" # Script is now in source/_scripts/ - scripts_dir = Path(__file__).parent + scripts_dir = Path(__file__).resolve().parent doc_dir = scripts_dir.parent.parent # from source/_scripts/ -> source/ -> doc/ output_dir = scripts_dir.parent / 'reference' / 'python_api' # source/reference/python_api output_dir.mkdir(parents=True, exist_ok=True) @@ -435,7 +435,7 @@ def main(): def generate_docs_no_clean(): """Generate documentation without cleaning first.""" # Script is now in source/_scripts/ - scripts_dir = Path(__file__).parent + scripts_dir = Path(__file__).resolve().parent doc_dir = scripts_dir.parent.parent # from source/_scripts/ -> source/ -> doc/ output_dir = scripts_dir.parent / 'reference' / 'python_api' # source/reference/python_api output_dir.mkdir(parents=True, exist_ok=True) diff --git a/doc/source/conf.py b/doc/source/conf.py index a4c6fbb0..2cacd783 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -85,7 +85,9 @@ ] # Source file suffixes -source_suffix = ['.rst', '.md'] +# Keep .md before .rst so duplicated docnames (e.g. index.md + index.rst) +# resolve to Markdown, which is where the Java API landing content lives. +source_suffix = ['.md', '.rst'] # API documentation settings # Templates for API documentation @@ -100,6 +102,7 @@ exclude_patterns = [ '_build', '.ipynb_checkpoints', + 'reference/java_api/index.rst', ] # -- Options for HTML output ------------------------------------------------- diff --git a/doc/source/development/code_style_guide.md b/doc/source/development/code_style_guide.md index 5b52b0c1..dd2af96b 100644 --- a/doc/source/development/code_style_guide.md +++ b/doc/source/development/code_style_guide.md @@ -11,12 +11,20 @@ We follow the [Google C++ Style Guide](https://google.github.io/styleguide/cppgu We follow the [black](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html) code style for Python coding standards. +## Java Style + +Java code in NeuG follows [Google Java Style](https://google.github.io/styleguide/javaguide.html), +enforced by the Maven Spotless plugin (with `google-java-format`). + +Java source is located at [tools/java_driver](../../tools/java_driver). + ## Style Linter and Checker GraphScope uses different linters and checkers for each language to enforce code style rules: - C++: [clang-format-8](https://releases.llvm.org/8.0.0/tools/clang/docs/ClangFormat.html) and [cpplint](https://github.com/cpplint/cpplint) - Python: [Flake8](https://flake8.pycqa.org/en/latest/) +- Java: [Spotless Maven Plugin](https://github.com/diffplug/spotless/tree/main/plugin-maven) Each linter can be included in the build process to ensure that the code adheres to the style guide. Below are the commands to check the code style in each language: @@ -51,4 +59,22 @@ $ python3 -m isort --check --diff . $ python3 -m black --check --diff . $ python3 -m flake8 . $ popd +``` + +For Java: + +- Check style only: + +```bash +$ pushd tools/java_driver +$ mvn spotless:check +$ popd +``` + +- Auto-format Java files: + +```bash +$ pushd tools/java_driver +$ mvn spotless:apply +$ popd ``` \ No newline at end of file diff --git a/doc/source/reference/_meta.ts b/doc/source/reference/_meta.ts index 1f49d889..4370bc5d 100644 --- a/doc/source/reference/_meta.ts +++ b/doc/source/reference/_meta.ts @@ -1,5 +1,5 @@ export default { - python_api: "Python API", cpp_api: "C++ API", + python_api: "Python API", java_api: "Java API", }; diff --git a/doc/source/reference/cpp_api/connection.md b/doc/source/reference/cpp_api/connection.md index f3ee782b..974fefa3 100644 --- a/doc/source/reference/cpp_api/connection.md +++ b/doc/source/reference/cpp_api/connection.md @@ -83,7 +83,7 @@ if (result.has_value()) { - Use parameterized queries for dynamic values to prevent injection. - Specifying correct access_mode ensures proper transaction handling. -- **Returns:** `result` containing either: +- **Returns:** result containing either: - `QueryResult` with query results on success - Error status with message on failure diff --git a/doc/source/reference/cpp_api/neug_db.md b/doc/source/reference/cpp_api/neug_db.md index c11a53ce..37415411 100644 --- a/doc/source/reference/cpp_api/neug_db.md +++ b/doc/source/reference/cpp_api/neug_db.md @@ -188,3 +188,10 @@ Remove a connection from the database. - This method is used to remove a connection when it is closed, to remove the handle from the database. - This method is not thread-safe, so it should be called only when the connection is closed. And should be only called internally. +#### `CloseAllConnection()` + +Remove all connection from the database. + +- **Notes:** + - This method is used to remove all connection when tp svc created, to remove the handle from the database. + diff --git a/doc/source/reference/cpp_api/query_result.md b/doc/source/reference/cpp_api/query_result.md index 130716e7..2ae73bb4 100644 --- a/doc/source/reference/cpp_api/query_result.md +++ b/doc/source/reference/cpp_api/query_result.md @@ -2,178 +2,35 @@ **Full name:** `neug::QueryResult` -Container for Cypher query execution results. +Lightweight wrapper around protobuf `QueryResponse`. -`QueryResult` provides convenient access to query results through both iterator-style (hasNext/next) and random access (operator[]) patterns. Results are returned as `RecordLine` objects containing Entry pointers. - -**Usage Example:** -```cpp -auto result = conn->Query("MATCH (n:Person) RETURN n.name, n.age LIMIT 100"); -if (result.has_value()) { - QueryResult& qr = result.value(); - // Method 1: Range-based for loop - for (auto& record : qr) { - std::cout << record.ToString() << std::endl; - } - // Method 2: Random access by index - for (size_t i = 0; i < qr.length(); ++i) { - RecordLine record = qr[i]; - } - // Get result schema - std::cout << "Schema: " << qr.get_result_schema() << std::endl; -} -``` - -**Memory Model:** -- `QueryResult` owns the underlying protobuf data -- `RecordLine` contains pointers to internal data (no copying) -- Do not access records after `QueryResult` is destroyed - -### Constructors & Destructors - -#### `QueryResult()=default` - -Default constructor creating empty result. - -- **Since:** v0.1.0 - -#### `QueryResult(results::CollectiveResults &&res)` - -Construct `QueryResult` from CollectiveResults. - -- **Parameters:** - - `res`: CollectiveResults to be moved and stored - -- **Since:** v0.1.0 +``QueryResult`` stores a full query response and exposes utility methods for: +- constructing from serialized protobuf bytes (``From()``), +- obtaining row count (``length()``), +- accessing response schema (``result_schema()``), +- serializing/deserializing (``Serialize()`` / ``From()``), +- debugging output (``ToString()``). +It does not provide row-iterator semantics such as `hasNext()/next()`. ### Public Methods -#### `From(results::CollectiveResults &&result)` - -Create `QueryResult` from CollectiveResults (move semantics). - -Factory method that creates a `QueryResult` by moving a CollectiveResults. -`QueryResult` constructor with `std::move`. - -- **Parameters:** - - `result`: CollectiveResults to be moved into the `QueryResult` - -- **Returns:** `QueryResult` containing the moved results - -- **Since:** v0.1.0 - -#### `From(const std::string &result_str)` - -Create `QueryResult` by deserializing from a string. - -Deserializes a CollectiveResults protobuf from string format. - -- **Parameters:** - - `result_str`: Serialized CollectiveResults string - -- **Throws:** - - `std::runtime_error`: if parsing fails - -- **Returns:** `QueryResult` containing the deserialized results - -- **Since:** v0.1.0 - -#### `hasNext() const` - -Check if there are more records to iterate. - -- **Returns:** `true` if `cur_index_` < `result_`.results_size(), `false` otherwise - -- **Since:** v0.1.0 - -#### `next()` - -Get the next result record and advance iterator. - -Returns a `RecordLine` containing pointers to Entry objects from the current record. Advances `cur_index_` after retrieving the record. - -- **Notes:** - - Returns pointers to internal data - no memory allocation or copying - - Returns empty `RecordLine` if no more records (logs error) - - Caller should check `hasNext()` before calling this method - -- **Returns:** `RecordLine` containing `const` Entry* pointers to record columns - -- **Since:** v0.1.0 - -#### `operator[](int index) const` - -Get a record by index (random access). - -- **Parameters:** - - `index`: Zero-based index of the record to retrieve - -- **Notes:** - - Does not affect iterator state (`cur_index_`) - - Returns pointers to internal data - no copying - -- **Returns:** `RecordLine` containing `const` Entry* pointers to record columns +#### `ToString() const` -- **Since:** v0.1.0 +Convert entire result set to string. #### `length() const` -Get total number of records in the result set. - -- **Returns:** Total number of result records - -- **Since:** v0.1.0 - -#### `get_result_schema() const` - -Get the result schema as a string. - -- **Returns:** `const` `std::string`& Reference to the schema string from CollectiveResults - -- **Since:** v0.1.0 +Get total number of rows. +#### `result_schema() const` ---- - -## RecordLine - -**Full name:** `neug::RecordLine` - -A single row/record from query results. - -`RecordLine` represents one row of query output, containing multiple column values (entries). Each entry corresponds to a RETURN clause expression in the Cypher query. - -**Usage Example:** -```cpp -auto result = conn->Query("MATCH (n:Person) RETURN n.name, n.age", "read"); -for (auto& record : result.value()) { - // Access entries by index - const auto& entries = record.entries(); - // entries[0] = name, entries[1] = age - std::cout << record.ToString() << std::endl; -} -``` - -### Constructors & Destructors - -#### `RecordLine(std::vector< const results::Entry * > entries)` - -Construct `RecordLine` from entry pointers. - -- **Parameters:** - - `entries`: Vector of pointers to result entries - -### Public Methods - -#### `ToString() const` - -Convert record to string representation. +Get result schema metadata. -- **Returns:** String representation of all entries +#### `response() const` -#### `entries() const` +Get underlying protobuf response. -Get all entries (column values) in this record. +#### `Serialize() const` -- **Returns:** Vector of `const` pointers to Entry objects +Serialize entire result set to string. diff --git a/doc/source/reference/cpp_api/service.md b/doc/source/reference/cpp_api/service.md index d23b0c87..1d5767f0 100644 --- a/doc/source/reference/cpp_api/service.md +++ b/doc/source/reference/cpp_api/service.md @@ -253,7 +253,7 @@ auto param_result = guard->Eval(query); - **Parameters:** - `query`: JSON string containing query, access_mode, and parameters -- **Returns:** Result containing CollectiveResults on success, or error status +- **Returns:** Result containing `QueryResult` on success, or error status --- diff --git a/doc/source/reference/java_api/index.md b/doc/source/reference/java_api/index.md index d1dabfff..01094cb5 100644 --- a/doc/source/reference/java_api/index.md +++ b/doc/source/reference/java_api/index.md @@ -24,7 +24,7 @@ The Java driver is designed for application integration and service-side usage: ## Deployment Model -The current Java SDK supports **remote access over HTTP only**. +The current Java SDK supports **remote access over HTTP only**, i.e., **service mode**. - **Supported**: connect to a running NeuG server with `GraphDatabase.driver("http://host:port")` - **Not supported**: embedded/in-process database access from Java @@ -171,14 +171,6 @@ try (Session session = driver.session()) { } ``` -## Reference Pages - -- [Driver](driver) -- [Config](config) -- [Session](session) -- [ResultSet](result_set) -- [ResultSetMetaData](result_set_metadata) - ## Dependencies The Java driver depends on the following libraries: diff --git a/doc/source/reference/python_api/connection.md b/doc/source/reference/python_api/connection.md index d435d9cb..400284e2 100644 --- a/doc/source/reference/python_api/connection.md +++ b/doc/source/reference/python_api/connection.md @@ -4,7 +4,7 @@ The Neug connection module. - + ## Connection Objects @@ -48,7 +48,7 @@ Close the connection. ```python def execute(query: str, access_mode="", - parameters: dict[str, Any] | None = None) -> QueryResult + parameters: Optional[Dict[str, Any]] = None) -> QueryResult ``` Execute a cypher query on the database. User could specify multiple queries in a single string, diff --git a/doc/source/reference/python_api/query_result.md b/doc/source/reference/python_api/query_result.md index 49a51432..a1bca058 100644 --- a/doc/source/reference/python_api/query_result.md +++ b/doc/source/reference/python_api/query_result.md @@ -18,8 +18,7 @@ It has the following methods to iterate over the results. - `hasNext()`: Returns True if there are more results to iterate over. - `getNext()`: Returns the next result as a list. - `length()`: Returns the total number of results. - - `get_result_schema()`: Returns the schema of the result, which is a - yaml string describing the structure of the result. + - `column_names()`: Returns the projected column names as strings. ```python @@ -32,19 +31,15 @@ It has the following methods to iterate over the results. ``` - + -### get\_result\_schema +### column\_names ```python -def get_result_schema() -> str +def column_names() ``` -Get the schema of the result. - -- **Returns:** - - **str** - The schema of the result, which is a yaml string describing the structure of the result. +Return the projected column names as a list of strings. @@ -55,6 +50,7 @@ def get_bolt_response() -> str ``` Get the result in Bolt response format. +TODO(zhanglei,xiaoli): Make sure the format consistency with neo4j bolt response. - **Returns:** - **str** diff --git a/doc/source/reference/python_api/session.md b/doc/source/reference/python_api/session.md index 738f794b..628842bc 100644 --- a/doc/source/reference/python_api/session.md +++ b/doc/source/reference/python_api/session.md @@ -2,7 +2,7 @@ # Module neug.session - + ## Session Objects diff --git a/include/neug/main/query_result.h b/include/neug/main/query_result.h index 38d4a5d5..c72f950f 100644 --- a/include/neug/main/query_result.h +++ b/include/neug/main/query_result.h @@ -26,14 +26,16 @@ namespace neug { /** - * @brief QueryResult purely based on C++ Arrow Table. + * @brief Lightweight wrapper around protobuf `QueryResponse`. * - * Provides iterator-style access to query results stored in Arrow format. - * Supports hasNext()/next() pattern for sequential iteration and random - * access via operator[]. + * `QueryResult` stores a full query response and exposes utility methods for: + * - constructing from serialized protobuf bytes (`From()`), + * - obtaining row count (`length()`), + * - accessing response schema (`result_schema()`), + * - serializing/deserializing (`Serialize()` / `From()`), + * - debugging output (`ToString()`). * - * The underlying Arrow Table may have chunked columns. This implementation - * combines chunks for easier access. + * It does not provide row-iterator semantics such as `hasNext()/next()`. */ class QueryResult { public: @@ -67,9 +69,19 @@ class QueryResult { */ size_t length() const { return response_.row_count(); } + /** + * @brief Get result schema metadata. + */ const neug::MetaDatas& result_schema() const { return response_.schema(); } + + /** + * @brief Get underlying protobuf response. + */ const neug::QueryResponse& response() const { return response_; } + /** + * @brief Serialize entire result set to string. + */ std::string Serialize() const; private: diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java index 48c8179e..fe6e0f62 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java @@ -16,7 +16,6 @@ import java.math.BigDecimal; import java.sql.Date; import java.sql.Timestamp; -import java.util.List; /** * A cursor over the results of a database query. @@ -335,13 +334,6 @@ public interface ResultSet extends AutoCloseable { */ boolean isClosed(); - /** - * Retrieves the names of all columns in this ResultSet. - * - * @return a list of column names - */ - List getColumnNames(); - /** Moves the cursor to the end of this ResultSet object, just after the last row. */ void afterLast(); diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java index 78eed706..a1113a24 100644 --- a/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -487,16 +487,6 @@ public boolean wasNull() { return was_null; } - @Override - public List getColumnNames() { - Results.MetaDatas metaDatas = response.getSchema(); - List columnNames = new ArrayList<>(); - for (int i = 0; i < metaDatas.getNameCount(); i++) { - columnNames.add(metaDatas.getName(i)); - } - return columnNames; - } - @Override public void close() { closed = true; diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java index e42ea63b..f5ce67fb 100644 --- a/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java @@ -19,7 +19,6 @@ import com.google.protobuf.ByteString; import java.math.BigDecimal; import java.util.Arrays; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -131,14 +130,6 @@ public void testGetInt() { assertEquals(30, resultSet.getInt(1)); } - @Test - public void testGetColumnNames() { - List columnNames = resultSet.getColumnNames(); - assertEquals(2, columnNames.size()); - assertEquals("name", columnNames.get(0)); - assertEquals("age", columnNames.get(1)); - } - @Test public void testClose() { assertFalse(resultSet.isClosed()); From 4b154c99cd35cb524de487a08c7a6eb11ae27329 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 19 Mar 2026 19:28:10 +0800 Subject: [PATCH 54/60] fix --- doc/source/reference/java_api/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/reference/java_api/index.md b/doc/source/reference/java_api/index.md index 01094cb5..6bed09d9 100644 --- a/doc/source/reference/java_api/index.md +++ b/doc/source/reference/java_api/index.md @@ -24,7 +24,7 @@ The Java driver is designed for application integration and service-side usage: ## Deployment Model -The current Java SDK supports **remote access over HTTP only**, i.e., **service mode**. +The current Java SDK supports **remote access over HTTP only**, i.e., [**service mode**](../../getting_started/getting_started.md#service-mode). - **Supported**: connect to a running NeuG server with `GraphDatabase.driver("http://host:port")` - **Not supported**: embedded/in-process database access from Java From e70b45edfae312f1b387d5fa855695967d2bb5e8 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 19 Mar 2026 20:08:07 +0800 Subject: [PATCH 55/60] Implement the iteration method for QueryResult --- include/neug/main/query_result.h | 73 ++++++++++++++- src/main/query_result.cc | 150 +++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+), 2 deletions(-) diff --git a/include/neug/main/query_result.h b/include/neug/main/query_result.h index c72f950f..29b87cf4 100644 --- a/include/neug/main/query_result.h +++ b/include/neug/main/query_result.h @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -33,12 +34,64 @@ namespace neug { * - obtaining row count (`length()`), * - accessing response schema (`result_schema()`), * - serializing/deserializing (`Serialize()` / `From()`), - * - debugging output (`ToString()`). + * - debugging output (`ToString()`), + * - read-only row traversal via C++ range-for (`begin()/end()`). * - * It does not provide row-iterator semantics such as `hasNext()/next()`. + * Note: traversal currently provides row index + column access to raw protobuf + * arrays through `RowView`, rather than materialized typed cell values. */ + +class RowView { + public: + RowView(const neug::QueryResponse* response, size_t row_index) + : response_(response), row_index_(row_index) {} + + std::string ToString() const; + + private: + const neug::QueryResponse* response_ = nullptr; + size_t row_index_ = 0; +}; class QueryResult { public: + class const_iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = RowView; + using difference_type = std::ptrdiff_t; + using pointer = void; + using reference = RowView; + + const_iterator() = default; + const_iterator(const neug::QueryResponse* response, size_t row_index) + : response_(response), row_index_(row_index) {} + + value_type operator*() const { return RowView(response_, row_index_); } + + const_iterator& operator++() { + ++row_index_; + return *this; + } + + const_iterator operator++(int) { + const_iterator tmp(*this); + ++(*this); + return tmp; + } + + bool operator==(const const_iterator& other) const { + return response_ == other.response_ && row_index_ == other.row_index_; + } + + bool operator!=(const const_iterator& other) const { + return !(*this == other); + } + + private: + const neug::QueryResponse* response_ = nullptr; + size_t row_index_ = 0; + }; + static QueryResult From(std::string&& serialized_table); static QueryResult From(const std::string& serialized_table); @@ -84,6 +137,22 @@ class QueryResult { */ std::string Serialize() const; + /** + * @brief Begin iterator for range-for traversal by row index. + */ + const_iterator begin() const { return const_iterator(&response_, 0); } + + /** + * @brief End iterator for range-for traversal by row index. + */ + const_iterator end() const { + return const_iterator(&response_, + static_cast(response_.row_count())); + } + + const_iterator cbegin() const { return begin(); } + const_iterator cend() const { return end(); } + private: neug::QueryResponse response_; }; diff --git a/src/main/query_result.cc b/src/main/query_result.cc index 03b333ac..db225c34 100644 --- a/src/main/query_result.cc +++ b/src/main/query_result.cc @@ -40,6 +40,156 @@ namespace neug { +static bool is_valid(const std::string& validity_map, size_t row_index) { + return validity_map.empty() || + validity_map[row_index / 8] & (1 << (row_index % 8)); +} + +static void get_value(const neug::Array& array, size_t row_index, + std::stringstream& ss) { + switch (array.typed_array_case()) { + case neug::Array::kInt32Array: { + if (!is_valid(array.int32_array().validity(), row_index)) { + ss << "null"; + break; + } else { + ss << array.int32_array().values(row_index); + } + break; + } + case neug::Array::kUint32Array: { + if (!is_valid(array.uint32_array().validity(), row_index)) { + ss << "null"; + break; + } else { + ss << array.uint32_array().values(row_index); + } + break; + } + case neug::Array::kInt64Array: { + if (!is_valid(array.int64_array().validity(), row_index)) { + ss << "null"; + break; + } else { + ss << array.int64_array().values(row_index); + } + break; + } + case neug::Array::kUint64Array: { + if (!is_valid(array.uint64_array().validity(), row_index)) { + ss << "null"; + break; + } else { + ss << array.uint64_array().values(row_index); + } + break; + } + case neug::Array::kFloatArray: { + if (!is_valid(array.float_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.float_array().values(row_index); + } + break; + } + case neug::Array::kDoubleArray: { + if (!is_valid(array.double_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.double_array().values(row_index); + } + break; + } + case neug::Array::kStringArray: { + if (!is_valid(array.string_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.string_array().values(row_index); + } + break; + } + case neug::Array::kBoolArray: { + if (!is_valid(array.bool_array().validity(), row_index)) { + ss << "null"; + } else { + ss << (array.bool_array().values(row_index) ? "true" : "false"); + } + break; + } + case neug::Array::kDateArray: { + if (!is_valid(array.date_array().validity(), row_index)) { + ss << "null"; + } else { + ss << Date(array.date_array().values(row_index)).to_string(); + } + break; + } + case neug::Array::kTimestampArray: { + if (!is_valid(array.timestamp_array().validity(), row_index)) { + ss << "null"; + } else { + ss << DateTime(array.timestamp_array().values(row_index)).to_string(); + } + break; + } + case neug::Array::kIntervalArray: { + if (!is_valid(array.interval_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.interval_array().values(row_index); + } + break; + } + case neug::Array::kVertexArray: { + if (!is_valid(array.vertex_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.vertex_array().values(row_index); + } + break; + } + case neug::Array::kEdgeArray: { + if (!is_valid(array.edge_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.edge_array().values(row_index); + } + break; + } + case neug::Array::kPathArray: { + if (!is_valid(array.path_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.path_array().values(row_index); + } + break; + } + default: { + LOG(WARNING) << "Unsupported array type in RowView: " + << array.typed_array_case(); + ss << "null"; + } + } +} + +std::string RowView::ToString() const { + if (response_ == nullptr) { + THROW_RUNTIME_ERROR("RowView has null response"); + } + if (row_index_ >= response_->row_count()) { + THROW_RUNTIME_ERROR("Row index out of range"); + } + std::stringstream ss; + for (int i = 0; i < response_->arrays_size(); ++i) { + const auto& array = response_->arrays(i); + get_value(array, row_index_, ss); + if (i < response_->arrays_size() - 1) { + ss << ", "; + } + } + return ss.str(); +} + std::string QueryResult::ToString() const { return response_.DebugString(); } QueryResult QueryResult::From(const std::string& serialized_table) { From ad18239856d09a7b89ccb27c36fb890249b764c4 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 19 Mar 2026 20:30:49 +0800 Subject: [PATCH 56/60] update query_result.md --- doc/source/reference/cpp_api/query_result.md | 13 ++++++++++-- include/neug/main/query_result.h | 22 ++++++++++---------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/doc/source/reference/cpp_api/query_result.md b/doc/source/reference/cpp_api/query_result.md index 2ae73bb4..3a60bb37 100644 --- a/doc/source/reference/cpp_api/query_result.md +++ b/doc/source/reference/cpp_api/query_result.md @@ -9,8 +9,9 @@ Lightweight wrapper around protobuf `QueryResponse`. - obtaining row count (``length()``), - accessing response schema (``result_schema()``), - serializing/deserializing (``Serialize()`` / ``From()``), -- debugging output (``ToString()``). -It does not provide row-iterator semantics such as `hasNext()/next()`. +- debugging output (``ToString()``), +- read-only row traversal via C++ range-for (``begin()`/end()`). +Note: traversal currently provides row index + column access to raw protobuf arrays through ``RowView``, rather than materialized typed cell values. ### Public Methods @@ -34,3 +35,11 @@ Get underlying protobuf response. Serialize entire result set to string. +#### `begin() const` + +Begin iterator for range-for traversal by row index. + +#### `end() const` + +End iterator for range-for traversal by row index. + diff --git a/include/neug/main/query_result.h b/include/neug/main/query_result.h index 29b87cf4..8c5b7f46 100644 --- a/include/neug/main/query_result.h +++ b/include/neug/main/query_result.h @@ -25,6 +25,17 @@ #include "neug/generated/proto/response/response.pb.h" namespace neug { +class RowView { + public: + RowView(const neug::QueryResponse* response, size_t row_index) + : response_(response), row_index_(row_index) {} + + std::string ToString() const; + + private: + const neug::QueryResponse* response_ = nullptr; + size_t row_index_ = 0; +}; /** * @brief Lightweight wrapper around protobuf `QueryResponse`. @@ -41,17 +52,6 @@ namespace neug { * arrays through `RowView`, rather than materialized typed cell values. */ -class RowView { - public: - RowView(const neug::QueryResponse* response, size_t row_index) - : response_(response), row_index_(row_index) {} - - std::string ToString() const; - - private: - const neug::QueryResponse* response_ = nullptr; - size_t row_index_ = 0; -}; class QueryResult { public: class const_iterator { From ebe9b7ea72faa0b49ae95c3e40bed2e063e0fd2c Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Thu, 19 Mar 2026 20:36:21 +0800 Subject: [PATCH 57/60] update --- doc/source/reference/cpp_api/connection.md | 4 ++-- doc/source/reference/cpp_api/index.md | 2 +- doc/source/reference/cpp_api/neug_db.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/reference/cpp_api/connection.md b/doc/source/reference/cpp_api/connection.md index 974fefa3..c2a69a29 100644 --- a/doc/source/reference/cpp_api/connection.md +++ b/doc/source/reference/cpp_api/connection.md @@ -61,8 +61,8 @@ result = conn->Query("MATCH (p:Person) WHERE p.age > $min_age RETURN p", "read", params); // Process results if (result.has_value()) { - for (auto& record : result.value()) { - // Access columns via record.entries() + for (const auto& record : result.value()) { + } } else { std::cerr << "Query failed: " << result.error().message() << std::endl; diff --git a/doc/source/reference/cpp_api/index.md b/doc/source/reference/cpp_api/index.md index 865f46f9..2320883c 100644 --- a/doc/source/reference/cpp_api/index.md +++ b/doc/source/reference/cpp_api/index.md @@ -43,7 +43,7 @@ int main() { // Process results if (result.has_value()) { - for (auto& record : result.value()) { + for (const auto& record : result.value()) { std::cout << record.ToString() << std::endl; } } diff --git a/doc/source/reference/cpp_api/neug_db.md b/doc/source/reference/cpp_api/neug_db.md index 37415411..c62dc845 100644 --- a/doc/source/reference/cpp_api/neug_db.md +++ b/doc/source/reference/cpp_api/neug_db.md @@ -15,7 +15,7 @@ db.Open("/path/to/data", 4); // 4 threads auto conn = db.Connect(); auto result = conn->Query("MATCH (n:Person) RETURN n LIMIT 10"); // Process results -for (auto& record : result.value()) { +for (const auto& record : result.value()) { std::cout << record.ToString() << std::endl; } // Close database (persists data) From ee3368fc86a6b9d8bd9a3d921652c5f95ae19b9e Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Fri, 20 Mar 2026 12:47:57 +0800 Subject: [PATCH 58/60] update doc --- doc/source/reference/java_api/index.md | 53 +++++++++++++------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/doc/source/reference/java_api/index.md b/doc/source/reference/java_api/index.md index 6bed09d9..325f52d3 100644 --- a/doc/source/reference/java_api/index.md +++ b/doc/source/reference/java_api/index.md @@ -46,7 +46,7 @@ mvn clean install -DskipTests com.alibaba.neug neug-java-driver - 1.0.0-SNAPSHOT + ${neug.version} ``` @@ -87,31 +87,7 @@ public class Example { Before using the Java SDK, start a NeuG HTTP server that exposes the query endpoint. You can use either the C++ binary or the Python API to start the server. -### Option A: Start with the C++ binary - -#### 1. Build the server binary - -From the repository root: - -```bash -cmake -S . -B build -DBUILD_EXECUTABLES=ON -DBUILD_HTTP_SERVER=ON -cmake --build build --target rt_server -j -``` - -#### 2. Start the server - -```bash -./build/bin/rt_server --data-path /path/to/graph --http-port 10000 --host 0.0.0.0 --shard-num 16 -``` - -Common options: - -- `--data-path`: path to the NeuG data directory -- `--http-port`: HTTP port for Java clients, default is `10000` -- `--host`: bind address, default is `127.0.0.1` -- `--shard-num`: shard number of actor system, default is `9` - -### Option B: Start with Python +### Option A: Start with Python If you have the `neug` Python package installed, you can start the server directly from Python: @@ -140,6 +116,31 @@ except KeyboardInterrupt: db.stop_serving() ``` + +### Option B: Start with the C++ binary + +#### 1. Build the server binary + +From the repository root: + +```bash +cmake -S . -B build -DBUILD_EXECUTABLES=ON -DBUILD_HTTP_SERVER=ON +cmake --build build --target rt_server -j +``` + +#### 2. Start the server + +```bash +./build/bin/rt_server --data-path /path/to/graph --http-port 10000 --host 0.0.0.0 --shard-num 16 +``` + +Common options: + +- `--data-path`: path to the NeuG data directory +- `--http-port`: HTTP port for Java clients, default is `10000` +- `--host`: bind address, default is `127.0.0.1` +- `--shard-num`: shard number of actor system, default is `9` + > **Note:** Make sure all local connections are closed before calling `db.serve()`. > Once the server is running, no new local connections are allowed until `db.stop_serving()` is called. From 3cbb9630b6e045db13ecba61bd91b75d14a87d01 Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Fri, 20 Mar 2026 14:15:12 +0800 Subject: [PATCH 59/60] format example --- doc/source/reference/java_api/index.md | 9 +++++---- doc/source/reference/java_api/session.md | 13 +++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/doc/source/reference/java_api/index.md b/doc/source/reference/java_api/index.md index 325f52d3..7ddcc7df 100644 --- a/doc/source/reference/java_api/index.md +++ b/doc/source/reference/java_api/index.md @@ -71,10 +71,11 @@ public class Example { try (Driver driver = GraphDatabase.driver("http://localhost:10000")) { driver.verifyConnectivity(); - try (Session session = driver.session(); - ResultSet rs = session.run("RETURN 1 AS value")) { - while (rs.next()) { - System.out.println(rs.getInt("value")); + try (Session session = driver.session()) { + try (ResultSet rs = session.run("RETURN 1 AS value")) { + while (rs.next()) { + System.out.println(rs.getInt("value")); + } } } } diff --git a/doc/source/reference/java_api/session.md b/doc/source/reference/java_api/session.md index ec58c002..24cf60ef 100644 --- a/doc/source/reference/java_api/session.md +++ b/doc/source/reference/java_api/session.md @@ -25,12 +25,13 @@ try (Session session = driver.session(); ```java import java.util.Map; -try (Session session = driver.session(); - ResultSet rs = session.run( - "MATCH (n) WHERE n.name = $name RETURN n.age AS age", - Map.of("name", "marko"))) { - while (rs.next()) { - System.out.println(rs.getLong("age")); +try (Session session = driver.session()) { + try (ResultSet rs = session.run( + "MATCH (n) WHERE n.name = $name RETURN n.age AS age", + Map.of("name", "marko"))) { + while (rs.next()) { + System.out.println(rs.getLong("age")); + } } } ``` From 8939f1987e57f7f50168fb05b665a7f2f8f50dbe Mon Sep 17 00:00:00 2001 From: liulx20 <519459125@qq.com> Date: Fri, 20 Mar 2026 14:17:46 +0800 Subject: [PATCH 60/60] format --- doc/source/reference/java_api/result_set.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/source/reference/java_api/result_set.md b/doc/source/reference/java_api/result_set.md index 62f92bde..dde577be 100644 --- a/doc/source/reference/java_api/result_set.md +++ b/doc/source/reference/java_api/result_set.md @@ -5,12 +5,13 @@ ## Common Access Pattern ```java -try (Session session = driver.session(); - ResultSet rs = session.run("MATCH (n:Person) RETURN n.name AS name, n.age AS age")) { - while (rs.next()) { - String name = rs.getString("name"); - long age = rs.getLong("age"); - System.out.println(name + ", " + age); +try (Session session = driver.session()) { + try (ResultSet rs = session.run("MATCH (n:Person) RETURN n.name AS name, n.age AS age")) { + while (rs.next()) { + String name = rs.getString("name"); + long age = rs.getLong("age"); + System.out.println(name + ", " + age); + } } } ```