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