Metadata-Version: 2.4
Name: cypherast
Version: 0.1.9
Summary: Cypher/GQL transpiler, rewriter, planner, and in-memory executor
Project-URL: Homepage, https://github.com/gauravsagar483/cypherast
Project-URL: Repository, https://github.com/gauravsagar483/cypherast
Project-URL: Issues, https://github.com/gauravsagar483/cypherast/issues
Project-URL: Release notes, https://github.com/gauravsagar483/cypherast/releases
Author: Gaurav Sagar
Maintainer: Gaurav Sagar
License-Expression: MIT
License-File: LICENSE
Keywords: cypher,gql,graph,opencypher,parser,transpiler
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Compilers
Classifier: Typing :: Typed
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: mypy>=1.14; extra == 'dev'
Requires-Dist: pre-commit>=4.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.9; extra == 'dev'
Description-Content-Type: text/markdown

# cypherast

Cypher/GQL transpiler, rewriter, cost-based planner, and in-memory executor.

Zero runtime dependencies. Python 3.11+. Graph-native API — no SQL vocabulary.

## Install

```bash
# from source (dev)
uv sync --group dev

# from PyPI (after a release tag)
uv add cypherast
# or: uv pip install cypherast
```

## Quick start

```python
import cypherast

q = "MATCH (n:Person) WHERE n.age > 30 RETURN n.name"
tree = cypherast.parse_one(q)
print(tree.cypher(pretty=True))

print(cypherast.optimize(q).cypher(pretty=True))

print(cypherast.translate(q, from_="opencypher", to_="neo4j", pretty=True))
# alias:
print(cypherast.transpile(q, from_="opencypher", to_="memgraph"))
```

## Public API

| Function | Purpose |
|----------|---------|
| `parse` / `parse_one` | Cypher text → AST |
| `translate` / `transpile` | Cross-dialect rewrite |
| `optimize` | Canonicalizer rewriter passes |
| `explain` / `profile` | Cost / naive plan text |
| `run` | Execute on in-memory `Graph` |
| `lineage` | Binding-level provenance |

Full samples: [docs/api.md](docs/api.md). Guides: [docs/](docs/README.md) (AST primer, optimizer, dialects, onboarding).

### parse / parse_one

```python
tree = cypherast.parse_one(
    "MATCH (a:Person)-[:KNOWS]->(b) RETURN a.name, b.name",
    read="opencypher",  # or neo4j / memgraph / puppygraph
)
print(tree.cypher())

# Procedure CALL (openCypher / Neo4j / Memgraph / PuppyGraph algo.*)
# Distinct from CALL { subquery }. YIELD bindings are in-scope for optimize/validate.
proc = cypherast.parse_one(
    "CALL algo.wcc({labels: ['User'], relationshipTypes: ['LINK']}) "
    "YIELD id, componentId RETURN id, componentId"
)
print(proc.cypher())
```

### translate (transpile)

```python
out = cypherast.translate(
    "MATCH (n:Person) RETURN n",
    from_="opencypher",
    to_="neo4j",
    pretty=True,
)
print(out)
```

### optimize

Folds `WHERE n.x = lit` into `(n {x: lit})`, simplifies expressions, then applies write-dialect
constraints. By default **`strict=True`**: leftover dialect/schema issues raise `ValidationError`
(same codes as `validate`). Pass `strict=False` for a soft rewritten AST.

Rules are named and toggleable:

```python
from cypherast.optimizer import RULES, OPTIONAL_RULES
from cypherast.schema import GraphSchema

print(
    cypherast.optimize(
        "MATCH (n:Person) WHERE n.status = 'ACTIVE' RETURN n"
    ).cypher(pretty=True)
)
# MATCH (n:Person {status: 'ACTIVE'}) RETURN n

# disable / only
cypherast.optimize(q, disable=["qualify", "annotate_types"])
cypherast.optimize(q, write="puppygraph", constraint_disable=["strip_nulls_order_modifiers"])
cypherast.optimize(q, rules=RULES + OPTIONAL_RULES)  # opt-in merge_match_chains

# optional graph catalog (id fields; labels/rels/props when schema.strict=True)
schema = GraphSchema()
schema.add_label("Person", name="string")
schema.add_id_field("DataQualityCheck", "dq_check_id")
cypherast.optimize(q, write="puppygraph", schema=schema)
```

### explain / profile / run

```python
from cypherast.executor import Graph

print(cypherast.explain("MATCH (n:Person)-[:KNOWS]->(m) RETURN n, m"))

g = Graph()
g.create_node(["Person"], {"name": "Ada", "age": 36})
rows = cypherast.run(
    "MATCH (n:Person) WHERE n.age > 30 RETURN n.name",
    graph=g,
)
print(list(rows))
```

### lineage

```python
root = cypherast.lineage(
    "MATCH (n:Person) RETURN n.name AS name",
    binding="name",
)
print(root)  # provenance Node; .to_html() for vis.js
```

## CLI

```bash
uv run cypherast parse "MATCH (n) RETURN n"
uv run cypherast translate "MATCH (n) RETURN n" -r opencypher -w neo4j --pretty
uv run cypherast optimize "MATCH (n:Person) WHERE n.x = 1 RETURN n"
uv run cypherast explain "MATCH (a)-[:R]->(b) RETURN a"
uv run cypherast run "CREATE (n:Person {name: 'Ada'}) RETURN n"
```

Or via Make:

```bash
make help
make sync test
make check
make optimize Q="MATCH (n:Person) RETURN n.name"
make optimize Q="..." CONSTRAINT_DISABLE=strip_nulls_order_modifiers
make optimize Q="..." DISABLE=qualify,annotate_types
make validate Q="MATCH (n) RETURN n"
make translate Q="MATCH (n:Person) RETURN n" FROM=opencypher TO=puppygraph OPT=1
```

## Dialects

`opencypher` · `neo4j` · `memgraph` · `puppygraph` (read+write). Gremlin/GQL generators = v1.x.

openCypher 9 spec: [openCypher9.pdf](https://s3.amazonaws.com/artifacts.opencypher.org/openCypher9.pdf).

`puppygraph` subclasses openCypher and applies engine capability constraints on optimize/translate
(labelled MATCH, no Cartesian multi-path MATCH, strip `NULLS FIRST/LAST`, FET-45 null CASE, etc.).
Does **not** inject `LIMIT` or enforce hop caps (leave those to the engine / query_guard).

Procedure `CALL ns.proc(…) [YIELD …]` parses and renders for all dialects (including PuppyGraph
`algo.*`). Optimize leaves procedure calls as pass-through; run algorithms on the engine, not
the in-memory executor.

```python
cypherast.optimize(q, write="puppygraph").cypher(dialect="puppygraph")
cypherast.translate(q, from_="opencypher", to_="puppygraph", optimize=True)
cypherast.validate(q, dialect="puppygraph", schema=schema)  # schema= optional
```

## TCK scoreboard

Official [openCypher TCK](https://github.com/opencypher/openCypher/tree/master/tck) is **not vendored**. The runner clones it to `/tmp/opencypher` and writes `tests/tck/results.md`:

```bash
make test-tck-official          # parse + in-memory executor
make test-tck-official-parse    # parse gate only
make test-tck-oc9             # OC9-excluded scenario filter
```

Override feature path: `CYPHERAST_TCK_PATH=/path/to/tck/features`.

Recent scores (runnable scenarios exclude Cucumber Scenario Outline placeholders):

| Gate | Rate |
|------|------|
| Parse (1,339 real queries) | ~95% |
| Run (executable only) | ~62% |
| Effective run (+ expected errors) | ~65% |

**Run rate notes:** The runner skips outlines, side-effect checks, unparseable queries, and procedure stubs. Scenarios that expect compile/runtime errors count as passes when cypherast rejects the query (`expected` bucket).

## Status

v0.1.9 — openCypher 9 validation (CG1501–CG1512); official TCK runner (~95% parse); multidialect regression; PuppyGraph bound var-length rels.

## CI

GitHub Actions (`.github/workflows/ci.yml`): ruff · mypy (strict) · pytest + coverage (fail-under 60%, uploads `coverage.xml`).

Release (`.github/workflows/release.yml`): push tag `vX.Y.Z` (must match `project.version` in `pyproject.toml`) → quality gate → `uv build` → **PyPI** (trusted publishing) → GitHub Release with wheel/sdist.

Local: `make check` · `make test-cov` · `make build`

Dry-run build locally: `make dist` then `uvx twine check dist/*`.
