diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1c3d5b5ae..ef072c43f 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,18 @@ jobs: with: python-version: '3.10' + - name: Setup Java and Maven + uses: s4u/setup-maven-action@v1.19.0 + with: + java-version: '17' + maven-version: '3.9.6' - 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 +52,5 @@ 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 f78933d5b..9d6857db8 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -90,6 +90,12 @@ jobs: with: python-version: '3.10' + - name: Setup Java and Maven + uses: s4u/setup-maven-action@v1.19.0 + with: + java-version: '17' + maven-version: '3.9.6' + - name: Get PR Changes uses: dorny/paths-filter@v3 id: changes @@ -157,4 +163,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 060c2acde..a3084aef0 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' @@ -78,6 +80,7 @@ jobs: - 'proto/**' - 'third_party/**' - 'tools/python_bind/**' + - 'tools/java_driver/**' - 'tests/**' - '!tests/extension/**' - '!tests/extensions/**' @@ -102,7 +105,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- @@ -140,6 +143,12 @@ jobs: with: python-version: '3.13' + - name: Setup Java and Maven + uses: s4u/setup-maven-action@v1.19.0 + with: + java-version: '17' + 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') run: | @@ -347,6 +356,14 @@ 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 + cd ${GITHUB_WORKSPACE}/tools/python_bind/ + python3 -m pytest -sv tests/test_java_driver.py + # ======================================== # Phase 4: E2E Tests # ======================================== diff --git a/bin/benchmark.cc b/bin/benchmark.cc index a1cd6e5e6..8cf14b57b 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/_scripts/generate_cpp_docs.py b/doc/source/_scripts/generate_cpp_docs.py index a5440eb68..d202ead73 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", }; """ diff --git a/doc/source/_scripts/generate_python_docs.py b/doc/source/_scripts/generate_python_docs.py index 6149e5a25..8064e35f6 100644 --- a/doc/source/_scripts/generate_python_docs.py +++ b/doc/source/_scripts/generate_python_docs.py @@ -96,7 +96,7 @@ def clean_generated_docs(output_dir: Path, scripts_dir: Path): def generate_docs(): """Generate documentation for all modules using pydoc-markdown.""" # Script is now in source/_scripts/ - scripts_dir = Path(__file__).parent + scripts_dir = Path(__file__).resolve().parent doc_dir = scripts_dir.parent.parent # from source/_scripts/ -> source/ -> doc/ output_dir = scripts_dir.parent / 'reference' / 'python_api' # source/reference/python_api output_dir.mkdir(parents=True, exist_ok=True) @@ -435,7 +435,7 @@ def main(): def generate_docs_no_clean(): """Generate documentation without cleaning first.""" # Script is now in source/_scripts/ - scripts_dir = Path(__file__).parent + scripts_dir = Path(__file__).resolve().parent doc_dir = scripts_dir.parent.parent # from source/_scripts/ -> source/ -> doc/ output_dir = scripts_dir.parent / 'reference' / 'python_api' # source/reference/python_api output_dir.mkdir(parents=True, exist_ok=True) diff --git a/doc/source/conf.py b/doc/source/conf.py index a4c6fbb03..2cacd783a 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -85,7 +85,9 @@ ] # Source file suffixes -source_suffix = ['.rst', '.md'] +# Keep .md before .rst so duplicated docnames (e.g. index.md + index.rst) +# resolve to Markdown, which is where the Java API landing content lives. +source_suffix = ['.md', '.rst'] # API documentation settings # Templates for API documentation @@ -100,6 +102,7 @@ exclude_patterns = [ '_build', '.ipynb_checkpoints', + 'reference/java_api/index.rst', ] # -- Options for HTML output ------------------------------------------------- diff --git a/doc/source/development/code_style_guide.md b/doc/source/development/code_style_guide.md index 5b52b0c14..dd2af96b5 100644 --- a/doc/source/development/code_style_guide.md +++ b/doc/source/development/code_style_guide.md @@ -11,12 +11,20 @@ We follow the [Google C++ Style Guide](https://google.github.io/styleguide/cppgu We follow the [black](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html) code style for Python coding standards. +## Java Style + +Java code in NeuG follows [Google Java Style](https://google.github.io/styleguide/javaguide.html), +enforced by the Maven Spotless plugin (with `google-java-format`). + +Java source is located at [tools/java_driver](../../tools/java_driver). + ## Style Linter and Checker GraphScope uses different linters and checkers for each language to enforce code style rules: - C++: [clang-format-8](https://releases.llvm.org/8.0.0/tools/clang/docs/ClangFormat.html) and [cpplint](https://github.com/cpplint/cpplint) - Python: [Flake8](https://flake8.pycqa.org/en/latest/) +- Java: [Spotless Maven Plugin](https://github.com/diffplug/spotless/tree/main/plugin-maven) Each linter can be included in the build process to ensure that the code adheres to the style guide. Below are the commands to check the code style in each language: @@ -51,4 +59,22 @@ $ python3 -m isort --check --diff . $ python3 -m black --check --diff . $ python3 -m flake8 . $ popd +``` + +For Java: + +- Check style only: + +```bash +$ pushd tools/java_driver +$ mvn spotless:check +$ popd +``` + +- Auto-format Java files: + +```bash +$ pushd tools/java_driver +$ mvn spotless:apply +$ popd ``` \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst index dbb5d3e54..23c9c2c81 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -81,6 +81,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 9fab68169..4370bc5d3 100644 --- a/doc/source/reference/_meta.ts +++ b/doc/source/reference/_meta.ts @@ -1,4 +1,5 @@ export default { cpp_api: "C++ API", python_api: "Python API", + java_api: "Java API", }; diff --git a/doc/source/reference/cpp_api/connection.md b/doc/source/reference/cpp_api/connection.md index f3ee782bd..c2a69a297 100644 --- a/doc/source/reference/cpp_api/connection.md +++ b/doc/source/reference/cpp_api/connection.md @@ -61,8 +61,8 @@ result = conn->Query("MATCH (p:Person) WHERE p.age > $min_age RETURN p", "read", params); // Process results if (result.has_value()) { - for (auto& record : result.value()) { - // Access columns via record.entries() + for (const auto& record : result.value()) { + } } else { std::cerr << "Query failed: " << result.error().message() << std::endl; @@ -83,7 +83,7 @@ if (result.has_value()) { - Use parameterized queries for dynamic values to prevent injection. - Specifying correct access_mode ensures proper transaction handling. -- **Returns:** `result` containing either: +- **Returns:** result containing either: - `QueryResult` with query results on success - Error status with message on failure diff --git a/doc/source/reference/cpp_api/index.md b/doc/source/reference/cpp_api/index.md index 865f46f90..2320883c2 100644 --- a/doc/source/reference/cpp_api/index.md +++ b/doc/source/reference/cpp_api/index.md @@ -43,7 +43,7 @@ int main() { // Process results if (result.has_value()) { - for (auto& record : result.value()) { + for (const auto& record : result.value()) { std::cout << record.ToString() << std::endl; } } diff --git a/doc/source/reference/cpp_api/neug_db.md b/doc/source/reference/cpp_api/neug_db.md index c11a53ce8..c62dc8459 100644 --- a/doc/source/reference/cpp_api/neug_db.md +++ b/doc/source/reference/cpp_api/neug_db.md @@ -15,7 +15,7 @@ db.Open("/path/to/data", 4); // 4 threads auto conn = db.Connect(); auto result = conn->Query("MATCH (n:Person) RETURN n LIMIT 10"); // Process results -for (auto& record : result.value()) { +for (const auto& record : result.value()) { std::cout << record.ToString() << std::endl; } // Close database (persists data) @@ -188,3 +188,10 @@ Remove a connection from the database. - This method is used to remove a connection when it is closed, to remove the handle from the database. - This method is not thread-safe, so it should be called only when the connection is closed. And should be only called internally. +#### `CloseAllConnection()` + +Remove all connection from the database. + +- **Notes:** + - This method is used to remove all connection when tp svc created, to remove the handle from the database. + diff --git a/doc/source/reference/cpp_api/query_result.md b/doc/source/reference/cpp_api/query_result.md index 130716e74..3a60bb372 100644 --- a/doc/source/reference/cpp_api/query_result.md +++ b/doc/source/reference/cpp_api/query_result.md @@ -2,178 +2,44 @@ **Full name:** `neug::QueryResult` -Container for Cypher query execution results. +Lightweight wrapper around protobuf `QueryResponse`. -`QueryResult` provides convenient access to query results through both iterator-style (hasNext/next) and random access (operator[]) patterns. Results are returned as `RecordLine` objects containing Entry pointers. - -**Usage Example:** -```cpp -auto result = conn->Query("MATCH (n:Person) RETURN n.name, n.age LIMIT 100"); -if (result.has_value()) { - QueryResult& qr = result.value(); - // Method 1: Range-based for loop - for (auto& record : qr) { - std::cout << record.ToString() << std::endl; - } - // Method 2: Random access by index - for (size_t i = 0; i < qr.length(); ++i) { - RecordLine record = qr[i]; - } - // Get result schema - std::cout << "Schema: " << qr.get_result_schema() << std::endl; -} -``` - -**Memory Model:** -- `QueryResult` owns the underlying protobuf data -- `RecordLine` contains pointers to internal data (no copying) -- Do not access records after `QueryResult` is destroyed - -### Constructors & Destructors - -#### `QueryResult()=default` - -Default constructor creating empty result. - -- **Since:** v0.1.0 - -#### `QueryResult(results::CollectiveResults &&res)` - -Construct `QueryResult` from CollectiveResults. - -- **Parameters:** - - `res`: CollectiveResults to be moved and stored - -- **Since:** v0.1.0 +``QueryResult`` stores a full query response and exposes utility methods for: +- constructing from serialized protobuf bytes (``From()``), +- obtaining row count (``length()``), +- accessing response schema (``result_schema()``), +- serializing/deserializing (``Serialize()`` / ``From()``), +- debugging output (``ToString()``), +- read-only row traversal via C++ range-for (``begin()`/end()`). +Note: traversal currently provides row index + column access to raw protobuf arrays through ``RowView``, rather than materialized typed cell values. ### Public Methods -#### `From(results::CollectiveResults &&result)` - -Create `QueryResult` from CollectiveResults (move semantics). - -Factory method that creates a `QueryResult` by moving a CollectiveResults. -`QueryResult` constructor with `std::move`. - -- **Parameters:** - - `result`: CollectiveResults to be moved into the `QueryResult` - -- **Returns:** `QueryResult` containing the moved results - -- **Since:** v0.1.0 - -#### `From(const std::string &result_str)` - -Create `QueryResult` by deserializing from a string. - -Deserializes a CollectiveResults protobuf from string format. - -- **Parameters:** - - `result_str`: Serialized CollectiveResults string - -- **Throws:** - - `std::runtime_error`: if parsing fails - -- **Returns:** `QueryResult` containing the deserialized results - -- **Since:** v0.1.0 - -#### `hasNext() const` - -Check if there are more records to iterate. - -- **Returns:** `true` if `cur_index_` < `result_`.results_size(), `false` otherwise - -- **Since:** v0.1.0 - -#### `next()` - -Get the next result record and advance iterator. - -Returns a `RecordLine` containing pointers to Entry objects from the current record. Advances `cur_index_` after retrieving the record. - -- **Notes:** - - Returns pointers to internal data - no memory allocation or copying - - Returns empty `RecordLine` if no more records (logs error) - - Caller should check `hasNext()` before calling this method - -- **Returns:** `RecordLine` containing `const` Entry* pointers to record columns - -- **Since:** v0.1.0 - -#### `operator[](int index) const` - -Get a record by index (random access). - -- **Parameters:** - - `index`: Zero-based index of the record to retrieve - -- **Notes:** - - Does not affect iterator state (`cur_index_`) - - Returns pointers to internal data - no copying - -- **Returns:** `RecordLine` containing `const` Entry* pointers to record columns +#### `ToString() const` -- **Since:** v0.1.0 +Convert entire result set to string. #### `length() const` -Get total number of records in the result set. - -- **Returns:** Total number of result records - -- **Since:** v0.1.0 - -#### `get_result_schema() const` +Get total number of rows. -Get the result schema as a string. +#### `result_schema() const` -- **Returns:** `const` `std::string`& Reference to the schema string from CollectiveResults +Get result schema metadata. -- **Since:** v0.1.0 +#### `response() const` +Get underlying protobuf response. ---- - -## RecordLine - -**Full name:** `neug::RecordLine` - -A single row/record from query results. - -`RecordLine` represents one row of query output, containing multiple column values (entries). Each entry corresponds to a RETURN clause expression in the Cypher query. - -**Usage Example:** -```cpp -auto result = conn->Query("MATCH (n:Person) RETURN n.name, n.age", "read"); -for (auto& record : result.value()) { - // Access entries by index - const auto& entries = record.entries(); - // entries[0] = name, entries[1] = age - std::cout << record.ToString() << std::endl; -} -``` - -### Constructors & Destructors - -#### `RecordLine(std::vector< const results::Entry * > entries)` - -Construct `RecordLine` from entry pointers. - -- **Parameters:** - - `entries`: Vector of pointers to result entries - -### Public Methods - -#### `ToString() const` +#### `Serialize() const` -Convert record to string representation. +Serialize entire result set to string. -- **Returns:** String representation of all entries +#### `begin() const` -#### `entries() const` +Begin iterator for range-for traversal by row index. -Get all entries (column values) in this record. +#### `end() const` -- **Returns:** Vector of `const` pointers to Entry objects +End iterator for range-for traversal by row index. diff --git a/doc/source/reference/cpp_api/service.md b/doc/source/reference/cpp_api/service.md index d23b0c878..1d5767f0b 100644 --- a/doc/source/reference/cpp_api/service.md +++ b/doc/source/reference/cpp_api/service.md @@ -253,7 +253,7 @@ auto param_result = guard->Eval(query); - **Parameters:** - `query`: JSON string containing query, access_mode, and parameters -- **Returns:** Result containing CollectiveResults on success, or error status +- **Returns:** Result containing `QueryResult` on success, or error status --- diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst index ce1a1030d..fe6e3bce1 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/_meta.ts b/doc/source/reference/java_api/_meta.ts new file mode 100644 index 000000000..388613abc --- /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 000000000..60040a29d --- /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 000000000..84d8b8411 --- /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) diff --git a/doc/source/reference/java_api/index.md b/doc/source/reference/java_api/index.md new file mode 100644 index 000000000..7ddcc7df2 --- /dev/null +++ b/doc/source/reference/java_api/index.md @@ -0,0 +1,198 @@ +# 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. + +```{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: + +- **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` + +## Deployment Model + +The current Java SDK supports **remote access over HTTP only**, i.e., [**service mode**](../../getting_started/getting_started.md#service-mode). + +- **Supported**: connect to a running NeuG server with `GraphDatabase.driver("http://host:port")` +- **Not supported**: embedded/in-process database access from Java + +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 + +```bash +cd tools/java_driver +mvn clean install -DskipTests +``` + +### Add dependency in another Maven project + +```xml + + com.alibaba.neug + neug-java-driver + ${neug.version} + +``` + +## Core Interfaces + +- **[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 + +```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()) { + try (ResultSet rs = session.run("RETURN 1 AS value")) { + while (rs.next()) { + System.out.println(rs.getInt("value")); + } + } + } + } + } +} +``` + +## 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 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() +``` + + +### Option B: Start with the C++ binary + +#### 1. Build the server binary + +From the repository root: + +```bash +cmake -S . -B build -DBUILD_EXECUTABLES=ON -DBUILD_HTTP_SERVER=ON +cmake --build build --target rt_server -j +``` + +#### 2. Start the server + +```bash +./build/bin/rt_server --data-path /path/to/graph --http-port 10000 --host 0.0.0.0 --shard-num 16 +``` + +Common options: + +- `--data-path`: path to the NeuG data directory +- `--http-port`: HTTP port for Java clients, default is `10000` +- `--host`: bind address, default is `127.0.0.1` +- `--shard-num`: shard number of actor system, default is `9` + +> **Note:** Make sure all local connections are closed before calling `db.serve()`. +> Once the server is running, no new local connections are allowed until `db.stop_serving()` is called. + +### Connect from Java + +After the server is started via either option: + +```java +Driver driver = GraphDatabase.driver("http://localhost:10000"); +``` + +## 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")); + } + } +} +``` + +## 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 + +## 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 diff --git a/doc/source/reference/java_api/index.rst b/doc/source/reference/java_api/index.rst new file mode 100644 index 000000000..5cb4fe372 --- /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/doc/source/reference/java_api/result_set.md b/doc/source/reference/java_api/result_set.md new file mode 100644 index 000000000..dde577be8 --- /dev/null +++ b/doc/source/reference/java_api/result_set.md @@ -0,0 +1,57 @@ +# ResultSet + +`ResultSet` provides forward-only access to query results. + +## Common Access Pattern + +```java +try (Session session = driver.session()) { + try (ResultSet rs = session.run("MATCH (n:Person) RETURN n.name AS name, n.age AS age")) { + while (rs.next()) { + String name = rs.getString("name"); + long age = rs.getLong("age"); + System.out.println(name + ", " + age); + } + } +} +``` + +## 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 000000000..64a999869 --- /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 000000000..24cf60efb --- /dev/null +++ b/doc/source/reference/java_api/session.md @@ -0,0 +1,62 @@ +# 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()) { + try (ResultSet rs = session.run( + "MATCH (n) WHERE n.name = $name RETURN n.age AS age", + Map.of("name", "marko"))) { + while (rs.next()) { + System.out.println(rs.getLong("age")); + } + } +} +``` + +## 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) diff --git a/doc/source/reference/python_api/connection.md b/doc/source/reference/python_api/connection.md index d435d9cba..400284e2c 100644 --- a/doc/source/reference/python_api/connection.md +++ b/doc/source/reference/python_api/connection.md @@ -4,7 +4,7 @@ The Neug connection module. - + ## Connection Objects @@ -48,7 +48,7 @@ Close the connection. ```python def execute(query: str, access_mode="", - parameters: dict[str, Any] | None = None) -> QueryResult + parameters: Optional[Dict[str, Any]] = None) -> QueryResult ``` Execute a cypher query on the database. User could specify multiple queries in a single string, diff --git a/doc/source/reference/python_api/query_result.md b/doc/source/reference/python_api/query_result.md index 49a51432a..a1bca058a 100644 --- a/doc/source/reference/python_api/query_result.md +++ b/doc/source/reference/python_api/query_result.md @@ -18,8 +18,7 @@ It has the following methods to iterate over the results. - `hasNext()`: Returns True if there are more results to iterate over. - `getNext()`: Returns the next result as a list. - `length()`: Returns the total number of results. - - `get_result_schema()`: Returns the schema of the result, which is a - yaml string describing the structure of the result. + - `column_names()`: Returns the projected column names as strings. ```python @@ -32,19 +31,15 @@ It has the following methods to iterate over the results. ``` - + -### get\_result\_schema +### column\_names ```python -def get_result_schema() -> str +def column_names() ``` -Get the schema of the result. - -- **Returns:** - - **str** - The schema of the result, which is a yaml string describing the structure of the result. +Return the projected column names as a list of strings. @@ -55,6 +50,7 @@ def get_bolt_response() -> str ``` Get the result in Bolt response format. +TODO(zhanglei,xiaoli): Make sure the format consistency with neo4j bolt response. - **Returns:** - **str** diff --git a/doc/source/reference/python_api/session.md b/doc/source/reference/python_api/session.md index 738f794bf..628842bce 100644 --- a/doc/source/reference/python_api/session.md +++ b/doc/source/reference/python_api/session.md @@ -2,7 +2,7 @@ # Module neug.session - + ## Session Objects diff --git a/include/neug/main/query_result.h b/include/neug/main/query_result.h index 38d4a5d50..8c5b7f46a 100644 --- a/include/neug/main/query_result.h +++ b/include/neug/main/query_result.h @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -24,19 +25,73 @@ #include "neug/generated/proto/response/response.pb.h" namespace neug { +class RowView { + public: + RowView(const neug::QueryResponse* response, size_t row_index) + : response_(response), row_index_(row_index) {} + + std::string ToString() const; + + private: + const neug::QueryResponse* response_ = nullptr; + size_t row_index_ = 0; +}; /** - * @brief QueryResult purely based on C++ Arrow Table. + * @brief Lightweight wrapper around protobuf `QueryResponse`. * - * Provides iterator-style access to query results stored in Arrow format. - * Supports hasNext()/next() pattern for sequential iteration and random - * access via operator[]. + * `QueryResult` stores a full query response and exposes utility methods for: + * - constructing from serialized protobuf bytes (`From()`), + * - obtaining row count (`length()`), + * - accessing response schema (`result_schema()`), + * - serializing/deserializing (`Serialize()` / `From()`), + * - debugging output (`ToString()`), + * - read-only row traversal via C++ range-for (`begin()/end()`). * - * The underlying Arrow Table may have chunked columns. This implementation - * combines chunks for easier access. + * Note: traversal currently provides row index + column access to raw protobuf + * arrays through `RowView`, rather than materialized typed cell values. */ + class QueryResult { public: + class const_iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = RowView; + using difference_type = std::ptrdiff_t; + using pointer = void; + using reference = RowView; + + const_iterator() = default; + const_iterator(const neug::QueryResponse* response, size_t row_index) + : response_(response), row_index_(row_index) {} + + value_type operator*() const { return RowView(response_, row_index_); } + + const_iterator& operator++() { + ++row_index_; + return *this; + } + + const_iterator operator++(int) { + const_iterator tmp(*this); + ++(*this); + return tmp; + } + + bool operator==(const const_iterator& other) const { + return response_ == other.response_ && row_index_ == other.row_index_; + } + + bool operator!=(const const_iterator& other) const { + return !(*this == other); + } + + private: + const neug::QueryResponse* response_ = nullptr; + size_t row_index_ = 0; + }; + static QueryResult From(std::string&& serialized_table); static QueryResult From(const std::string& serialized_table); @@ -67,11 +122,37 @@ class QueryResult { */ size_t length() const { return response_.row_count(); } + /** + * @brief Get result schema metadata. + */ const neug::MetaDatas& result_schema() const { return response_.schema(); } + + /** + * @brief Get underlying protobuf response. + */ const neug::QueryResponse& response() const { return response_; } + /** + * @brief Serialize entire result set to string. + */ std::string Serialize() const; + /** + * @brief Begin iterator for range-for traversal by row index. + */ + const_iterator begin() const { return const_iterator(&response_, 0); } + + /** + * @brief End iterator for range-for traversal by row index. + */ + const_iterator end() const { + return const_iterator(&response_, + static_cast(response_.row_count())); + } + + const_iterator cbegin() const { return begin(); } + const_iterator cend() const { return end(); } + private: neug::QueryResponse response_; }; diff --git a/proto/response.proto b/proto/response.proto index 5bd5551c8..80e437cec 100644 --- a/proto/response.proto +++ b/proto/response.proto @@ -15,6 +15,8 @@ */ syntax="proto3"; package neug; +option java_package = "com.alibaba.neug.driver"; +option java_outer_classname = "Results"; option cc_generic_services = true; @@ -65,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; @@ -124,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; diff --git a/src/main/query_result.cc b/src/main/query_result.cc index 03b333ac5..db225c34c 100644 --- a/src/main/query_result.cc +++ b/src/main/query_result.cc @@ -40,6 +40,156 @@ namespace neug { +static bool is_valid(const std::string& validity_map, size_t row_index) { + return validity_map.empty() || + validity_map[row_index / 8] & (1 << (row_index % 8)); +} + +static void get_value(const neug::Array& array, size_t row_index, + std::stringstream& ss) { + switch (array.typed_array_case()) { + case neug::Array::kInt32Array: { + if (!is_valid(array.int32_array().validity(), row_index)) { + ss << "null"; + break; + } else { + ss << array.int32_array().values(row_index); + } + break; + } + case neug::Array::kUint32Array: { + if (!is_valid(array.uint32_array().validity(), row_index)) { + ss << "null"; + break; + } else { + ss << array.uint32_array().values(row_index); + } + break; + } + case neug::Array::kInt64Array: { + if (!is_valid(array.int64_array().validity(), row_index)) { + ss << "null"; + break; + } else { + ss << array.int64_array().values(row_index); + } + break; + } + case neug::Array::kUint64Array: { + if (!is_valid(array.uint64_array().validity(), row_index)) { + ss << "null"; + break; + } else { + ss << array.uint64_array().values(row_index); + } + break; + } + case neug::Array::kFloatArray: { + if (!is_valid(array.float_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.float_array().values(row_index); + } + break; + } + case neug::Array::kDoubleArray: { + if (!is_valid(array.double_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.double_array().values(row_index); + } + break; + } + case neug::Array::kStringArray: { + if (!is_valid(array.string_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.string_array().values(row_index); + } + break; + } + case neug::Array::kBoolArray: { + if (!is_valid(array.bool_array().validity(), row_index)) { + ss << "null"; + } else { + ss << (array.bool_array().values(row_index) ? "true" : "false"); + } + break; + } + case neug::Array::kDateArray: { + if (!is_valid(array.date_array().validity(), row_index)) { + ss << "null"; + } else { + ss << Date(array.date_array().values(row_index)).to_string(); + } + break; + } + case neug::Array::kTimestampArray: { + if (!is_valid(array.timestamp_array().validity(), row_index)) { + ss << "null"; + } else { + ss << DateTime(array.timestamp_array().values(row_index)).to_string(); + } + break; + } + case neug::Array::kIntervalArray: { + if (!is_valid(array.interval_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.interval_array().values(row_index); + } + break; + } + case neug::Array::kVertexArray: { + if (!is_valid(array.vertex_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.vertex_array().values(row_index); + } + break; + } + case neug::Array::kEdgeArray: { + if (!is_valid(array.edge_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.edge_array().values(row_index); + } + break; + } + case neug::Array::kPathArray: { + if (!is_valid(array.path_array().validity(), row_index)) { + ss << "null"; + } else { + ss << array.path_array().values(row_index); + } + break; + } + default: { + LOG(WARNING) << "Unsupported array type in RowView: " + << array.typed_array_case(); + ss << "null"; + } + } +} + +std::string RowView::ToString() const { + if (response_ == nullptr) { + THROW_RUNTIME_ERROR("RowView has null response"); + } + if (row_index_ >= response_->row_count()) { + THROW_RUNTIME_ERROR("Row index out of range"); + } + std::stringstream ss; + for (int i = 0; i < response_->arrays_size(); ++i) { + const auto& array = response_->arrays(i); + get_value(array, row_index_, ss); + if (i < response_->arrays_size() - 1) { + ss << ", "; + } + } + return ss.str(); +} + std::string QueryResult::ToString() const { return response_.DebugString(); } QueryResult QueryResult::From(const std::string& serialized_table) { diff --git a/tools/java_driver/USAGE.md b/tools/java_driver/USAGE.md new file mode 100644 index 000000000..a5421bc7f --- /dev/null +++ b/tools/java_driver/USAGE.md @@ -0,0 +1,111 @@ +# NeuG Java Driver Usage Guide + +## Using in Other Projects + + +1. Install to local Maven repository: +```bash +cd tools/java_driver +mvn clean install -DskipTests +``` + +2. Add dependency to your project's `pom.xml`: +```xml + + com.alibaba.neug + neug-java-driver + 1.0.0-SNAPSHOT + +``` + + +## Usage Examples + +### Basic Connection + +```java + +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 com.alibaba.neug.driver.*; +import com.alibaba.neug.driver.utils.*; + +public class ConfigExample { + public static void main(String[] args) { + Config config = Config.builder() + .withConnectionTimeoutMillis(3000) + .build(); + + Driver driver = GraphDatabase.driver("http://localhost:10000", config); + + try (Session session = driver.session()) { + // 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_driver/pom.xml b/tools/java_driver/pom.xml new file mode 100644 index 000000000..208ea5350 --- /dev/null +++ b/tools/java_driver/pom.xml @@ -0,0 +1,185 @@ + + + 4.0.0 + + com.alibaba.neug + neug-java-driver + 1.0.0-SNAPSHOT + jar + + NeuG Java Driver + Java driver for NeuG graph database + + + UTF-8 + 9 + 9 + 5.9.3 + 2.0.7 + 4.29.6 + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + + com.squareup.okhttp3 + okhttp + 4.11.0 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.mockito + mockito-core + 5.4.0 + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${maven.compiler.source} + ${maven.compiler.target} + UTF-8 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.5.0 + + + **/Results.java + + + + + attach-javadocs + + jar + + + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + ${project.basedir}/../../proto + + response.proto + + ${project.build.directory}/generated-sources/protobuf/java + false + + + + + compile + + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.1 + true + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.40.0 + + + + src/main/java/**/*.java + src/test/java/**/*.java + + + 1.17.0 + + + + + + + + + + + diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/Driver.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/Driver.java new file mode 100644 index 000000000..644069163 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/Driver.java @@ -0,0 +1,70 @@ +/** + * Copyright 2020 Alibaba Group Holding Limited. + * + *

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

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

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

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

Example usage: + * + *

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

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

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

After calling this method, the driver should not be used anymore. + */ + @Override + void close(); + + /** + * Checks whether the driver has been closed. + * + * @return {@code true} if the driver is closed, {@code false} otherwise + */ + boolean isClosed(); +} diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/GraphDatabase.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/GraphDatabase.java new file mode 100644 index 000000000..948146be8 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/GraphDatabase.java @@ -0,0 +1,81 @@ +/** + * 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 com.alibaba.neug.driver.internal.InternalDriver; +import com.alibaba.neug.driver.utils.Config; + +/** + * Main entry point for creating NeuG database driver connections. + * + *

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

Example usage: + * + *

{@code
+ * // Create a driver with default configuration
+ * Driver driver = GraphDatabase.driver("http://localhost:10000");
+ *
+ * // Create a driver with custom configuration
+ * Config config = Config.builder()
+ *     .withMaxConnectionPoolSize(10)
+ *     .build();
+ * Driver driver = GraphDatabase.driver("http://localhost:10000", config);
+ * }
+ */ +public final class GraphDatabase { + + private GraphDatabase() { + // Prevent instantiation + } + + /** + * Creates a new driver instance with default configuration. + * + * @param uri the URI of the NeuG database server (e.g., "http://localhost: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: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://"); + } + if (config == null) { + throw new IllegalArgumentException("Config cannot be null"); + } + return new InternalDriver(uri, config); + } +} 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 new file mode 100644 index 000000000..fe6e0f62c --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSet.java @@ -0,0 +1,393 @@ +/** + * 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 java.math.BigDecimal; +import java.sql.Date; +import java.sql.Timestamp; + +/** + * A cursor over the results of a database query. + * + *

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

Example usage: + * + *

{@code
+ * ResultSet results = session.run("MATCH (n:Person) RETURN n.name as name, n.age as age");
+ * while (results.next()) {
+ *     String name = results.getString("name");
+ *     int age = results.getInt("age");
+ *     System.out.println(name + " is " + age + " years old");
+ * }
+ * results.close();
+ * }
+ */ +public interface ResultSet extends AutoCloseable { + /** + * Moves the cursor forward one row from its current position. + * + * @return {@code true} if the new current row is valid; {@code false} if there are no more rows + */ + boolean next(); + + /** + * Moves the cursor backward one row from its current position. + * + * @return {@code true} if the new current row is valid; {@code false} if the cursor is before + * the first row + */ + boolean previous(); + + /** + * Moves the cursor to the given row number in this ResultSet. + * + * @param row the row number to move to (0-based) + * @return {@code true} if the cursor is moved to a valid row; {@code false} otherwise + */ + boolean absolute(int row); + + /** + * Moves the cursor a relative number of rows, either positive or negative. + * + * @param rows the number of rows to move (positive for forward, negative for backward) + * @return {@code true} if the cursor is moved to a valid row; {@code false} otherwise + */ + boolean relative(int rows); + + /** + * Retrieves the current row number. + * + * @return the current row number (0-based), or -1 if there is no current row + */ + int getRow(); + + /** + * Retrieves the value of the designated column as an Object. + * + * @param columnName the name of the column + * @return the column value + * @throws IllegalArgumentException if the column name is not valid + */ + Object getObject(String columnName); + + /** + * Retrieves the value of the designated column as an Object. + * + * @param columnIndex the column index (0-based) + * @return the column value + * @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; + * @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; + * @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; + * @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; + * @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; + * @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; + * @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; + * @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; + * @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; + * @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; + * @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. + * + * @return {@code true} if the last column value read was SQL NULL; {@code false} otherwise + */ + boolean wasNull(); + + /** Closes this ResultSet and releases all associated resources. */ + @Override + void close(); + + /** + * Checks whether the ResultSet has been closed. + * + * @return {@code true} if the ResultSet is closed, {@code false} otherwise + */ + boolean isClosed(); + + /** 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(); + + /** + * 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 000000000..94371e7b2 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/ResultSetMetaData.java @@ -0,0 +1,85 @@ +/** + * 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 com.alibaba.neug.driver.utils.Types; + +/** + * 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 native NeuG type. + * + * @param column the column index (0-based) + * @return the native NeuG type enum + * @throws IndexOutOfBoundsException if the column index is out of bounds + */ + Types 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 new file mode 100644 index 000000000..44370a7df --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/Session.java @@ -0,0 +1,88 @@ +/** + * 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 com.alibaba.neug.driver.utils.AccessMode; +import java.util.Map; + +/** + * A session for executing queries against a NeuG database. + * + *

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

Example usage: + * + *

{@code
+ * try (Session session = driver.session()) {
+ *     ResultSet results = session.run("MATCH (n:Person) WHERE n.age > $age RETURN n",
+ *         Map.of("age", 30));
+ *     while (results.next()) {
+ *         System.out.println(results.getObject("n").toString());
+ *     }
+ * }
+ * }
+ */ +public interface Session extends AutoCloseable { + /** + * Executes a Cypher statement and returns the results. + * + * @param statement the Cypher query to execute + * @return a {@link ResultSet} containing the query results + * @throws RuntimeException if the query fails + */ + ResultSet run(String statement); + + /** + * Executes a Cypher statement with configuration options. + * + * @param statement the Cypher query to execute + * @param 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 parameters); + + /** + * Executes a Cypher statement with a specific access mode. + * + * @param statement the Cypher query to execute + * @param mode the access mode (READ/INSERT/UPDATE/SCHEMA) + * @return a {@link ResultSet} containing the query results + * @throws RuntimeException if the query fails + */ + ResultSet run(String statement, AccessMode mode); + + /** + * Executes a parameterized Cypher statement with a specific access mode. + * + * @param statement the Cypher query to execute + * @param parameters query parameters as key-value pairs + * @param mode the access mode (READ/INSERT/UPDATE/SCHEMA) + * @return a {@link ResultSet} containing the query results + * @throws RuntimeException if the query fails + */ + ResultSet run(String statement, Map parameters, AccessMode mode); + + /** Closes the session and releases all associated resources. */ + @Override + void close(); + + /** + * Checks whether the session has been closed. + * + * @return {@code true} if the session is closed, {@code false} otherwise + */ + boolean isClosed(); +} diff --git a/tools/java_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 new file mode 100644 index 000000000..9223066cf --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalDriver.java @@ -0,0 +1,68 @@ +/** + * 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.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. + * + *

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 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) { + this.client = new Client(uri, config); + } + + @Override + public Session session() { + if (client.isClosed()) { + throw new IllegalStateException("Driver is already closed"); + } + return new InternalSession(client); + } + + @Override + public void verifyConnectivity() { + try (Session session = session(); + ResultSet rs = session.run("RETURN 1", null, AccessMode.READ)) { + // Execute query to verify connectivity, result is discarded + } + } + + @Override + public void close() { + client.close(); + } + + @Override + public boolean isClosed() { + return client.isClosed(); + } +} 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 new file mode 100644 index 000000000..a1113a24d --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalResultSet.java @@ -0,0 +1,751 @@ +/** + * 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.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; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Date; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 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.was_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; + } + currentIndex = response.getRowCount(); // move to after-last position + return false; + } + + public boolean previous() { + if (currentIndex - 1 >= 0) { + currentIndex--; + return true; + } + currentIndex = -1; // move to before-first position + 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 + checkRowIndex(); + checkIndex(columnIndex); + 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 void update_was_null(ByteString nullBitmap) { + was_null = + !nullBitmap.isEmpty() + && (nullBitmap.byteAt(currentIndex / 8) & (1 << (currentIndex % 8))) == 0; + } + + private Object getObject(Results.Array array, int rowIndex, boolean nullAlreadyHandled) + throws Exception { + switch (array.getTypedArrayCase()) { + case STRING_ARRAY: + { + if (!nullAlreadyHandled) { + ByteString nullBitmap = array.getStringArray().getValidity(); + update_was_null(nullBitmap); + } + return array.getStringArray().getValues(rowIndex); + } + case INT32_ARRAY: + { + if (!nullAlreadyHandled) { + ByteString nullBitmap = array.getInt32Array().getValidity(); + update_was_null(nullBitmap); + } + return array.getInt32Array().getValues(rowIndex); + } + case INT64_ARRAY: + { + if (!nullAlreadyHandled) { + ByteString nullBitmap = array.getInt64Array().getValidity(); + update_was_null(nullBitmap); + } + return array.getInt64Array().getValues(rowIndex); + } + case BOOL_ARRAY: + { + if (!nullAlreadyHandled) { + ByteString nullBitmap = array.getBoolArray().getValidity(); + update_was_null(nullBitmap); + } + return array.getBoolArray().getValues(rowIndex); + } + case FLOAT_ARRAY: + { + if (!nullAlreadyHandled) { + ByteString nullBitmap = array.getFloatArray().getValidity(); + update_was_null(nullBitmap); + } + return array.getFloatArray().getValues(rowIndex); + } + case DOUBLE_ARRAY: + { + if (!nullAlreadyHandled) { + ByteString nullBitmap = array.getDoubleArray().getValidity(); + update_was_null(nullBitmap); + } + return array.getDoubleArray().getValues(rowIndex); + } + case TIMESTAMP_ARRAY: + { + if (!nullAlreadyHandled) { + ByteString nullBitmap = array.getTimestampArray().getValidity(); + update_was_null(nullBitmap); + } + return array.getTimestampArray().getValues(rowIndex); + } + case DATE_ARRAY: + { + if (!nullAlreadyHandled) { + ByteString nullBitmap = array.getDateArray().getValidity(); + update_was_null(nullBitmap); + } + return array.getDateArray().getValues(rowIndex); + } + case LIST_ARRAY: + { + Results.ListArray listArray = array.getListArray(); + + if (!nullAlreadyHandled) { + ByteString nullBitmap = listArray.getValidity(); + update_was_null(nullBitmap); + } + + 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 (!nullAlreadyHandled) { + ByteString nullBitmap = structArray.getValidity(); + update_was_null(nullBitmap); + } + 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 (!nullAlreadyHandled) { + ByteString nullBitmap = vertexArray.getValidity(); + update_was_null(nullBitmap); + } + Map map = + mapper.readValue( + vertexArray.getValues(rowIndex), + new TypeReference>() {}); + return map; + } + case EDGE_ARRAY: + { + Results.EdgeArray edgeArray = array.getEdgeArray(); + ObjectMapper mapper = JsonUtil.getInstance(); + if (!nullAlreadyHandled) { + ByteString nullBitmap = edgeArray.getValidity(); + update_was_null(nullBitmap); + } + Map map = + mapper.readValue( + edgeArray.getValues(rowIndex), + new TypeReference>() {}); + return map; + } + case PATH_ARRAY: + { + Results.PathArray pathArray = array.getPathArray(); + ObjectMapper mapper = JsonUtil.getInstance(); + if (!nullAlreadyHandled) { + ByteString nullBitmap = pathArray.getValidity(); + update_was_null(nullBitmap); + } + Map map = + mapper.readValue( + pathArray.getValues(rowIndex), + new TypeReference>() {}); + return map; + } + case INTERVAL_ARRAY: + { + Results.IntervalArray intervalArray = array.getIntervalArray(); + if (!nullAlreadyHandled) { + ByteString nullBitmap = intervalArray.getValidity(); + update_was_null(nullBitmap); + } + return intervalArray.getValues(rowIndex); + } + case UINT32_ARRAY: + { + Results.UInt32Array uint32Array = array.getUint32Array(); + if (!nullAlreadyHandled) { + ByteString nullBitmap = uint32Array.getValidity(); + update_was_null(nullBitmap); + } + // Convert uint32 to long to avoid overflow + return Integer.toUnsignedLong(uint32Array.getValues(rowIndex)); + } + case UINT64_ARRAY: + { + Results.UInt64Array uint64Array = array.getUint64Array(); + if (!nullAlreadyHandled) { + ByteString nullBitmap = uint64Array.getValidity(); + update_was_null(nullBitmap); + } + // Convert uint64 to BigInteger to avoid overflow + long value = uint64Array.getValues(rowIndex); + return new BigInteger(Long.toUnsignedString(value)); + } + default: + throw new UnsupportedOperationException( + "Unsupported array type: " + array.getTypedArrayCase()); + } + } + + @Override + public int getInt(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getInt(columnIndex); + } + + @Override + public int getInt(int columnIndex) { + checkRowIndex(); + checkIndex(columnIndex); + Results.Array arr = response.getArrays(columnIndex); + if (arr.hasInt32Array()) { + Results.Int32Array array = arr.getInt32Array(); + ByteString nullBitmap = array.getValidity(); + int value = array.getValues(currentIndex); + update_was_null(nullBitmap); + return value; + } + return getNumericValue(arr).intValue(); + } + + @Override + public long getLong(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getLong(columnIndex); + } + + @Override + public long getLong(int columnIndex) { + checkRowIndex(); + checkIndex(columnIndex); + Results.Array arr = response.getArrays(columnIndex); + if (arr.hasInt64Array()) { + Results.Int64Array array = arr.getInt64Array(); + ByteString nullBitmap = array.getValidity(); + long value = array.getValues(currentIndex); + update_was_null(nullBitmap); + return value; + } + return getNumericValue(arr).longValue(); + } + + @Override + public String getString(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getString(columnIndex); + } + + @Override + public String getString(int columnIndex) { + checkRowIndex(); + checkIndex(columnIndex); + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasStringArray()) { + return getObject(columnIndex).toString(); + } + Results.StringArray array = arr.getStringArray(); + ByteString nullBitmap = array.getValidity(); + String value = array.getValues(currentIndex); + update_was_null(nullBitmap); + return value; + } + + @Override + public Date getDate(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getDate(columnIndex); + } + + @Override + public Date getDate(int columnIndex) { + checkRowIndex(); + checkIndex(columnIndex); + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasDateArray()) { + throw new ClassCastException("Column " + columnIndex + " is not of type date"); + } + Results.DateArray array = arr.getDateArray(); + ByteString nullBitmap = array.getValidity(); + long timestamp = array.getValues(currentIndex); + update_was_null(nullBitmap); + return new Date(timestamp); + } + + @Override + public Timestamp getTimestamp(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getTimestamp(columnIndex); + } + + @Override + public Timestamp getTimestamp(int columnIndex) { + checkRowIndex(); + checkIndex(columnIndex); + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasTimestampArray()) { + throw new ClassCastException("Column " + columnIndex + " is not of type timestamp"); + } + Results.TimestampArray array = arr.getTimestampArray(); + ByteString nullBitmap = array.getValidity(); + long timestamp = array.getValues(currentIndex); + update_was_null(nullBitmap); + return new Timestamp(timestamp); + } + + @Override + public boolean getBoolean(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getBoolean(columnIndex); + } + + @Override + public boolean getBoolean(int columnIndex) { + checkRowIndex(); + checkIndex(columnIndex); + Results.Array arr = response.getArrays(columnIndex); + if (!arr.hasBoolArray()) { + throw new ClassCastException("Column " + columnIndex + " is not of type boolean"); + } + Results.BoolArray array = arr.getBoolArray(); + ByteString nullBitmap = array.getValidity(); + boolean value = array.getValues(currentIndex); + update_was_null(nullBitmap); + return value; + } + + @Override + public double getDouble(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getDouble(columnIndex); + } + + @Override + public double getDouble(int columnIndex) { + checkRowIndex(); + checkIndex(columnIndex); + Results.Array arr = response.getArrays(columnIndex); + if (arr.hasFloatArray()) { + Results.FloatArray array = arr.getFloatArray(); + ByteString nullBitmap = array.getValidity(); + float value = array.getValues(currentIndex); + update_was_null(nullBitmap); + return value; + } + return getNumericValue(arr).doubleValue(); + } + + @Override + public float getFloat(String columnName) { + int columnIndex = getColumnIndex(columnName); + return getFloat(columnIndex); + } + + @Override + public float getFloat(int columnIndex) { + checkRowIndex(); + checkIndex(columnIndex); + Results.Array arr = response.getArrays(columnIndex); + if (arr.hasFloatArray()) { + Results.FloatArray array = arr.getFloatArray(); + ByteString nullBitmap = array.getValidity(); + float value = array.getValues(currentIndex); + update_was_null(nullBitmap); + 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) { + checkRowIndex(); + checkIndex(columnIndex); + Results.Array arr = response.getArrays(columnIndex); + Number value = getNumericValue(arr); + 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 was_null; + } + + @Override + public void close() { + closed = true; + } + + @Override + public boolean isClosed() { + return closed; + } + + private int getColumnIndex(String columnName) { + Results.MetaDatas metaDatas = response.getSchema(); + int columnCount = metaDatas.getNameCount(); + for (int i = 0; i < columnCount; i++) { + if (metaDatas.getName(i).equals(columnName)) { + return i; + } + } + 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); + } + } + + private void checkRowIndex() { + if (currentIndex < 0 || currentIndex >= response.getRowCount()) { + throw new IndexOutOfBoundsException("Cursor is not positioned on a valid row"); + } + } + + /** + * Generic method to extract numeric value from any numeric array type. + * + * @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(); + update_was_null(nullBitmap); + return array.getValues(currentIndex); + } + + if (arr.hasInt64Array()) { + Results.Int64Array array = arr.getInt64Array(); + nullBitmap = array.getValidity(); + update_was_null(nullBitmap); + return array.getValues(currentIndex); + } + + if (arr.hasFloatArray()) { + Results.FloatArray array = arr.getFloatArray(); + nullBitmap = array.getValidity(); + update_was_null(nullBitmap); + return array.getValues(currentIndex); + } + + if (arr.hasDoubleArray()) { + Results.DoubleArray array = arr.getDoubleArray(); + nullBitmap = array.getValidity(); + update_was_null(nullBitmap); + return array.getValues(currentIndex); + } + + if (arr.hasUint32Array()) { + Results.UInt32Array array = arr.getUint32Array(); + nullBitmap = array.getValidity(); + update_was_null(nullBitmap); + // 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(); + update_was_null(nullBitmap); + // 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"); + } + + @Override + public void afterLast() { + // Position the cursor just after the last row + currentIndex = response.getRowCount(); + } + + @Override + public void beforeFirst() { + currentIndex = -1; + } + + @Override + public boolean first() { + if (response.getRowCount() == 0) { + return false; + } + currentIndex = 0; + return true; + } + + @Override + public boolean last() { + currentIndex = response.getRowCount() - 1; + return currentIndex >= 0; + } + + @Override + public boolean isFirst() { + return currentIndex == 0 && response.getRowCount() != 0; + } + + @Override + public boolean isLast() { + return currentIndex == response.getRowCount() - 1 && response.getRowCount() != 0; + } + + @Override + public boolean isBeforeFirst() { + return currentIndex == -1 && response.getRowCount() != 0; + } + + @Override + public boolean isAfterLast() { + return currentIndex == response.getRowCount() && response.getRowCount() != 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; + private boolean closed; +} 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 000000000..f7b646dc1 --- /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 Types getColumnType(int column) { + validateColumnIndex(column); + return columnTypes.get(column); + } + + @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 new file mode 100644 index 000000000..f44e02cf6 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/internal/InternalSession.java @@ -0,0 +1,85 @@ +/** + * 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.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. + * + *

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) { + return run(query, null, null); + } + + @Override + public ResultSet run(String query, Map parameters) { + return run(query, parameters, null); + } + + @Override + public ResultSet run(String query, AccessMode mode) { + return run(query, null, mode); + } + + @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); + } + } + + @Override + public void close() { + closed = true; + } + + @Override + public boolean isClosed() { + return closed; + } +} diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/AccessMode.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/AccessMode.java new file mode 100644 index 000000000..b1cc44a59 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/AccessMode.java @@ -0,0 +1,37 @@ +/** + * 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 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_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 new file mode 100644 index 000000000..f6f085f9d --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Client.java @@ -0,0 +1,115 @@ +/** + * 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; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * 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 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 != null && uri.endsWith("/")) ? uri + "cypher" : uri + "/cypher"; + 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(); + } + + /** + * 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 { + 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()) { + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + ResponseBody responseBody = response.body(); + if (responseBody == null) { + throw new IOException("Response body is null"); + } + return responseBody.bytes(); + } + } + + /** + * 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(); + 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/Config.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Config.java new file mode 100644 index 000000000..b51f40e30 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Config.java @@ -0,0 +1,165 @@ +/** + * 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; + +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; + + /** + * 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; + } + + /** + * 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; + config.readTimeoutMillis = readTimeoutMillis; + config.writeTimeoutMillis = writeTimeoutMillis; + config.keepAliveIntervalMillis = keepAliveIntervalMillis; + config.maxConnectionPoolSize = maxConnectionPoolSize; + 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; + } + + private int connectionTimeoutMillis; + private int readTimeoutMillis; + private int writeTimeoutMillis; + private int keepAliveIntervalMillis; + private int maxConnectionPoolSize; +} diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/JsonUtil.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/JsonUtil.java new file mode 100644 index 000000000..ff445c568 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/JsonUtil.java @@ -0,0 +1,45 @@ +/** + * 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; + +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() {} + + 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; + } + + private static ObjectMapper initMapper() { + ObjectMapper mapper = new ObjectMapper(); + return mapper; + } +} diff --git a/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/QuerySerializer.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/QuerySerializer.java new file mode 100644 index 000000000..95d23f1b3 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/QuerySerializer.java @@ -0,0 +1,89 @@ +/** + * 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; + +import com.fasterxml.jackson.databind.ObjectMapper; +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 { + 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_driver/src/main/java/com/alibaba/neug/driver/utils/ResponseParser.java b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/ResponseParser.java new file mode 100644 index 000000000..9fc3663d4 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/ResponseParser.java @@ -0,0 +1,43 @@ +/** + * 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; + +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. + * + *

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); + return new InternalResultSet(queryResponse); + } catch (Exception e) { + throw new RuntimeException("Failed to parse response", e); + } + } +} 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 000000000..983723df9 --- /dev/null +++ b/tools/java_driver/src/main/java/com/alibaba/neug/driver/utils/Types.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.utils; + +/** + * Enumeration of data types supported by NeuG database. + * + *

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

Example usage: + * + *

{@code
+ * Types type = Types.STRING;
+ * String typeName = type.getTypeName(); // Returns "STRING"
+ * }
+ */ +public enum Types { + /** Any type - represents a value of unknown or dynamic type. */ + ANY("ANY"), + + /** 32-bit signed integer. */ + INT32("INT32"), + + /** 32-bit unsigned integer. */ + UINT32("UINT32"), + + /** 64-bit signed integer (long). */ + INT64("INT64"), + + /** 64-bit unsigned integer (unsigned long). */ + UINT64("UINT64"), + /** Boolean value (true/false). */ + BOOLEAN("BOOLEAN"), + + /** 32-bit floating point number. */ + FLOAT("FLOAT"), + + /** 64-bit floating point number (double precision). */ + DOUBLE("DOUBLE"), + + /** Variable-length character string. */ + STRING("STRING"), + /** Fixed-precision decimal number. */ + DECIMAL("DECIMAL"), + + /** Date value (year, month, day). */ + DATE("DATE"), + + /** Time value (hour, minute, second). */ + TIME("TIME"), + + /** Timestamp value (date and time). */ + TIMESTAMP("TIMESTAMP"), + /** Binary data (byte array). */ + BYTES("BYTES"), + + /** Null value - represents the absence of a value. */ + NULL("NULL"), + + /** List/array of values. */ + LIST("LIST"), + + /** Map/dictionary of key-value pairs. */ + MAP("MAP"), + /** Graph node/vertex. */ + NODE("NODE"), + + /** Graph edge/relationship. */ + EDGE("EDGE"), + + /** Graph path. */ + PATH("PATH"), + + /** Struct/record type. */ + STRUCT("STRUCT"), + + /** Interval type - represents a time interval. */ + INTERVAL("INTERVAL"), + + /** Other/unknown type. */ + OTHER("OTHER"); + + private final String typeName; + + /** + * Constructs a Types enum value. + * + * @param typeName the human-readable name of the type + */ + Types(String typeName) { + this.typeName = typeName; + } + + /** + * Returns the human-readable name of this type. + * + * @return the type name as a string + */ + public String getTypeName() { + return typeName; + } + + /** + * 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/AccessModeTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/AccessModeTest.java new file mode 100644 index 000000000..22af3a020 --- /dev/null +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/AccessModeTest.java @@ -0,0 +1,56 @@ +/** + * 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.utils.AccessMode; +import org.junit.jupiter.api.Test; + +/** Test class for {@link AccessMode}. */ +public class AccessModeTest { + + @Test + public void testAccessModeValues() { + 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/com/alibaba/neug/driver/ClientTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/ClientTest.java new file mode 100644 index 000000000..f94d3fb1f --- /dev/null +++ b/tools/java_driver/src/test/java/com/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 com.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +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}. */ +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()); + } +} diff --git a/tools/java_driver/src/test/java/com/alibaba/neug/driver/ConfigTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/ConfigTest.java new file mode 100644 index 000000000..4b4e3b0a5 --- /dev/null +++ b/tools/java_driver/src/test/java/com/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 com.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import com.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/com/alibaba/neug/driver/GraphDatabaseTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/GraphDatabaseTest.java new file mode 100644 index 000000000..35f25bf28 --- /dev/null +++ b/tools/java_driver/src/test/java/com/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 com.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import com.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/com/alibaba/neug/driver/InternalResultSetMetaDataTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetMetaDataTest.java new file mode 100644 index 000000000..f489b09f3 --- /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/InternalResultSetTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java new file mode 100644 index 000000000..f5ce67fb8 --- /dev/null +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/InternalResultSetTest.java @@ -0,0 +1,383 @@ +/** + * 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.google.protobuf.ByteString; +import java.math.BigDecimal; +import java.util.Arrays; +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 testClose() { + assertFalse(resultSet.isClosed()); + resultSet.close(); + assertTrue(resultSet.isClosed()); + } + + @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/com/alibaba/neug/driver/JavaDriverE2ETest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java new file mode 100644 index 000000000..5f142d3a1 --- /dev/null +++ b/tools/java_driver/src/test/java/com/alibaba/neug/driver/JavaDriverE2ETest.java @@ -0,0 +1,84 @@ +/** + * 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 static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.alibaba.neug.driver.utils.*; +import java.util.Map; +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"; + + 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()); + driver.verifyConnectivity(); + + try (Session session = driver.session(); + ResultSet resultSet = session.run("RETURN 1 AS value")) { + assertTrue(resultSet.next()); + assertEquals(1, resultSet.getInt("value")); + assertEquals(1L, resultSet.getObject(0)); + assertFalse(resultSet.wasNull()); + assertEquals(Types.INT64, resultSet.getMetaData().getColumnType(0)); + assertEquals("value", resultSet.getMetaData().getColumnName(0)); + assertFalse(resultSet.next()); + } + } + } + + @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()); + } + } +} 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 new file mode 100644 index 000000000..7ac498760 --- /dev/null +++ b/tools/java_driver/src/test/java/com/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 com.alibaba.neug.driver; + +import static org.junit.jupiter.api.Assertions.*; + +import com.alibaba.neug.driver.utils.JsonUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +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/com/alibaba/neug/driver/QuerySerializerTest.java b/tools/java_driver/src/test/java/com/alibaba/neug/driver/QuerySerializerTest.java new file mode 100644 index 000000000..e9454d8ac --- /dev/null +++ b/tools/java_driver/src/test/java/com/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 com.alibaba.neug.driver; + +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 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()); + } +} 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 000000000..9ae250b3f --- /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