ocpg 0.3.0
OpenCypher PetGraph - 100% openCypher-compliant graph database engine
Requires Python: >=3.8
Keywords: cypher, graph, database, opencypher, petgraph, neo4j

# ocpg - OpenCypher PetGraph

**ocpg** is the Python package for [nxcypher](https://crates.io/crates/nxcypher), a 100% openCypher-compliant graph database engine written in Rust.

**O**pen**C**ypher + **P**et**G**raph = Fast, memory-safe graph queries in Python.

## Features

- ✅ **100% openCypher TCK compliant** (3,893 scenarios passing)
- 🚀 **High performance** - Pure Rust implementation with Python bindings
- 🔒 **Memory safe** - No unsafe code, guaranteed by Rust
- 🧪 **Well tested** - Comprehensive test coverage
- 📦 **Easy to install** - Single `pip install` command
- 🐍 **Pythonic API** - Natural Python interface

## Installation

```bash
pip install ocpg
```

## Quick Start

```python
import ocpg

# Create a graph
g = ocpg.Graph()

# Create nodes and relationships
g.execute("""
    CREATE (alice:Person {name: 'Alice', age: 30})
    CREATE (bob:Person {name: 'Bob', age: 25})
    CREATE (alice)-[:KNOWS]->(bob)
""")

# Query the graph
result = g.execute("MATCH (p:Person) RETURN p.name, p.age ORDER BY p.age")

# Iterate over results
for row in result:
    print(f"Name: {row['p.name']}, Age: {row['p.age']}")

# Or convert to list
data = result.to_list()
print(data)
```

## API Reference

### `Graph`

The main graph database class.

#### Methods

- **`execute(query: str, params: dict = None) -> QueryResult`**

  Execute a Cypher query with optional parameters.

  ```python
  result = g.execute(
      "MATCH (n:Person) WHERE n.age > $min_age RETURN n",
      {"min_age": 18}
  )
  ```

- **`node_count() -> int`**

  Get the number of nodes in the graph.

- **`edge_count() -> int`**

  Get the number of relationships in the graph.

- **`clear()`**

  Remove all nodes and relationships from the graph.

- **`create_node(labels: list[str], properties: dict = None) -> int`** 🚀 **NEW**

  Create a node directly without parsing Cypher (bulk loading). **~99x faster** than CREATE queries.

  ```python
  # Create a node and get its ID
  alice_id = g.create_node(["Person"], {"name": "Alice", "age": 30})
  bob_id = g.create_node(["Person"], {"name": "Bob", "age": 25})

  # No properties
  empty_id = g.create_node(["Label"])
  ```

- **`create_relationship(from_id: int, to_id: int, rel_type: str, properties: dict = None) -> int`** 🚀 **NEW**

  Create a relationship directly without parsing Cypher (bulk loading). **~99x faster** than CREATE queries.

  ```python
  # Create nodes first
  alice_id = g.create_node(["Person"], {"name": "Alice"})
  bob_id = g.create_node(["Person"], {"name": "Bob"})

  # Create relationship between them
  edge_id = g.create_relationship(
      alice_id,
      bob_id,
      "KNOWS",
      {"since": 2020}
  )
  ```

### `QueryResult`

Query result object containing rows and columns.

#### Properties

- **`columns: list[str]`** - List of column names

#### Methods

- **`__len__() -> int`** - Number of rows
- **`__getitem__(index: int) -> dict`** - Get row by index
- **`__iter__()`** - Iterate over rows
- **`to_list() -> list[dict]`** - Convert all rows to a list of dictionaries

#### Example

```python
result = g.execute("MATCH (n) RETURN n, n.name LIMIT 5")

print(f"Columns: {result.columns}")  # ['n', 'n.name']
print(f"Rows: {len(result)}")        # 5

# Access by index
first_row = result[0]
print(first_row['n.name'])

# Iterate
for row in result:
    print(row['n.name'])

# Convert to list
all_data = result.to_list()
```

## Supported Data Types

Python types are automatically converted to/from Cypher types:

| Python Type | Cypher Type | Notes |
|------------|-------------|-------|
| `None` | `NULL` | - |
| `bool` | `BOOLEAN` | - |
| `int` | `INTEGER` | - |
| `float` | `FLOAT` | - |
| `str` | `STRING` | - |
| `list` | `LIST` | Nested lists supported |
| `dict` | `MAP` | - |

Graph types (Node, Relationship, Path) are returned as dictionaries with a `_type` field:

```python
result = g.execute("MATCH (n:Person {name: 'Alice'}) RETURN n")
node = result[0]['n']

print(node['_type'])        # 'Node'
print(node['id'])           # Node ID
print(node['labels'])       # ['Person']
print(node['properties'])   # {'name': 'Alice', 'age': 30}
```

## Advanced Examples

### Parameterized Queries

```python
# Create with parameters
g.execute(
    "CREATE (p:Person {name: $name, age: $age})",
    {"name": "Charlie", "age": 35}
)

# Query with parameters
result = g.execute(
    "MATCH (p:Person) WHERE p.age BETWEEN $min AND $max RETURN p",
    {"min": 25, "max": 35}
)
```

### Working with Relationships

```python
# Create relationship with properties
g.execute("""
    MATCH (a:Person {name: 'Alice'})
    MATCH (b:Person {name: 'Bob'})
    CREATE (a)-[:KNOWS {since: 2020, strength: 0.8}]->(b)
""")

# Query relationships
result = g.execute("""
    MATCH (a:Person)-[r:KNOWS]->(b:Person)
    RETURN a.name, r.since, r.strength, b.name
""")

for row in result:
    print(f"{row['a.name']} knows {row['b.name']} since {row['r.since']}")
```

### Aggregations

```python
result = g.execute("""
    MATCH (p:Person)
    RETURN
        count(p) as total,
        avg(p.age) as avg_age,
        min(p.age) as min_age,
        max(p.age) as max_age
""")

stats = result[0]
print(f"Total: {stats['total']}, Avg Age: {stats['avg_age']:.1f}")
```

### Path Queries

```python
# Find shortest path
result = g.execute("""
    MATCH path = shortestPath(
        (a:Person {name: 'Alice'})-[:KNOWS*]-(b:Person {name: 'Charlie'})
    )
    RETURN path
""")

path = result[0]['path']
print(f"Path length: {len(path['nodes']) - 1}")
```

## Bulk Loading for High Performance 🚀

For loading large amounts of data, use the native **bulk loader API** instead of Cypher queries.

### Performance Comparison

Benchmark: Creating 1,000 nodes

| Method | Time | Throughput | Speedup |
|--------|------|------------|---------|
| **Bulk Loader** | 0.0018s | **555K nodes/sec** | **99x faster** |
| Cypher Queries | 0.178s | 5.6K nodes/sec | baseline |

### Bulk Loading Example

```python
import ocpg
import time

g = ocpg.Graph()

# Traditional approach (slow)
start = time.time()
for i in range(1000):
    g.execute(f"CREATE (p:Person {{name: 'Person{i}', age: {20 + i}}})")
print(f"Cypher: {time.time() - start:.2f}s")

# Bulk loader approach (99x faster!)
g2 = ocpg.Graph()
start = time.time()
node_ids = []
for i in range(1000):
    node_id = g2.create_node(["Person"], {"name": f"Person{i}", "age": 20 + i})
    node_ids.append(node_id)
print(f"Bulk: {time.time() - start:.2f}s")

# Create relationships between nodes
for i in range(len(node_ids) - 1):
    g2.create_relationship(node_ids[i], node_ids[i + 1], "KNOWS", {})
```

### When to Use Bulk Loading

✅ **Use bulk loaders for:**
- Initial data import
- Migrating from other databases
- Loading CSV/JSON files
- Creating large test datasets
- Any time you create >100 nodes/relationships

❌ **Use Cypher for:**
- Complex pattern matching and creation
- Conditional logic (MERGE, conditional SET)
- When you need to match existing nodes first
- One-off operations
- When query readability matters more than performance

## Performance Tips

1. **Use bulk loaders** (`create_node`, `create_relationship`) for loading data - **99x faster**
2. **Use parameters** instead of string formatting for better performance and security
3. **Batch operations** when creating many nodes/relationships
4. **Use LIMIT** for large result sets
5. **Create indexes** for frequently queried properties (when index support is added)

## Development

Building from source:

```bash
# Install maturin
pip install maturin

# Build and install in development mode
cd python-bindings
maturin develop

# Build release wheel
maturin build --release

# Run tests
python -m pytest
```

## License

Apache 2.0 - See [LICENSE](../LICENSE) for details.

## Links

- [GitHub Repository](https://github.com/your-org/nxcypher-rust)
- [openCypher Specification](https://opencypher.org/)
- [Issue Tracker](https://github.com/your-org/nxcypher-rust/issues)