diff --git a/.gitattributes b/.gitattributes index 5803f6f23..5ca5349c8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -32,4 +32,9 @@ *.whl binary -.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file +.github/workflows/*.lock.yml linguist-generated=true merge=ours +# ANTLR-generated openCypher/agtype parsers (committed so devs don't need Java); +# vendored apache/age agtype decoder. Collapsed in diffs — see the age layer README. +packages/ai/src/ai/common/graph/age/_cypher/gen/** linguist-generated=true +packages/ai/src/ai/common/graph/age/_agtype/gen/** linguist-generated=true +packages/ai/src/ai/common/graph/age/_agtype/** linguist-vendored=true diff --git a/.github/workflows/_build.yaml b/.github/workflows/_build.yaml index b82400876..f54a1826e 100644 --- a/.github/workflows/_build.yaml +++ b/.github/workflows/_build.yaml @@ -215,6 +215,46 @@ jobs: echo "ROCKETRIDE_TEST_AZURE_CONTAINER=rocketride-test" } >> "$GITHUB_ENV" + # Postgres for the RocketRide cloud DB node integration suites (Linux + # only): pgvector for rocketride_sql/vector, Apache AGE for + # rocketride_graph. Same pattern as MinIO/Azurite above — env vars are + # exported to $GITHUB_ENV only after both databases are healthy, and + # RR_REQUIRE_DB_TESTS additionally turns the suites' unreachable-DB + # skip into a hard failure, so a container that fails to start can + # never let the safety-control tests go silently green. + # NB: close to, not exactly, the cloud pin (PG 16.14 + AGE 1.5.0 + + # pgvector 0.8.0 in ONE database); publishing that exact image to GHCR + # is a noted follow-up. AGE version here IS the pinned 1.5.0. + - name: Start Postgres (pgvector + AGE) for tests / Linux + if: matrix.platform == 'ubuntu' && inputs.codeql == false + run: | + set -euo pipefail + docker run -d --name rr-pgvector -p 55432:5432 \ + -e POSTGRES_USER=rruser -e POSTGRES_PASSWORD=rrpass -e POSTGRES_DB=rrtenant \ + pgvector/pgvector:pg16 + docker run -d --name rr-age -p 55433:5432 \ + -e POSTGRES_USER=rruser -e POSTGRES_PASSWORD=rrpass -e POSTGRES_DB=rrtenant \ + apache/age:release_PG16_1.5.0 -c shared_preload_libraries=age + for name in rr-pgvector rr-age; do + ready= + for i in $(seq 1 30); do + if docker exec "$name" pg_isready -U rruser -d rrtenant >/dev/null 2>&1; then ready=1; break; fi + sleep 2 + done + [ "${ready:-}" = "1" ] || { echo "$name did not become ready"; docker logs "$name" || true; exit 1; } + done + docker exec rr-pgvector psql -U rruser -d rrtenant -c 'CREATE EXTENSION IF NOT EXISTS vector;' + docker exec rr-age psql -U rruser -d rrtenant -c 'CREATE EXTENSION IF NOT EXISTS age;' + # Smoke both: a vector literal and an AGE graph round-trip. + docker exec rr-pgvector psql -U rruser -d rrtenant -c "SELECT '[1,2,3]'::vector;" + docker exec rr-age psql -U rruser -d rrtenant -c "SET search_path=ag_catalog,\"\$user\",public; SELECT create_graph('ci_smoke'); SELECT drop_graph('ci_smoke', true);" + # Export test vars only once both databases are live. + { + echo "RR_TEST_PG_DSN=postgresql://rruser:rrpass@localhost:55432/rrtenant" + echo "RR_TEST_AGE_DSN=postgresql://rruser:rrpass@localhost:55433/rrtenant" + echo "RR_REQUIRE_DB_TESTS=1" + } >> "$GITHUB_ENV" + - name: Test if: inputs.codeql == false # Integration tests boot a local server on :5565 and connect a test diff --git a/nodes/src/nodes/rocketride_graph/IGlobal.py b/nodes/src/nodes/rocketride_graph/IGlobal.py new file mode 100644 index 000000000..31577985f --- /dev/null +++ b/nodes/src/nodes/rocketride_graph/IGlobal.py @@ -0,0 +1,358 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""RocketRide cloud graph global state. + +Mirrors ``graph_neo4j`` on :class:`GraphGlobalBase`, with two differences: + +1. **Connection** — no host/user/password config; the per-tenant DSN comes + from the account layer (``ai.common.rocketride_db``), same seam as + ``rocketride_sql`` / ``rocketride_vector``. The tenant database is + Postgres + Apache AGE. +2. **Query paths** — Apache AGE cannot run bare Cypher, so ``_run_query`` + (safe), ``_run_query_raw`` (EXECUTE) and ``_validate_query`` all route + through the Cypher->AGE translation layer (``ai.common.graph.age``). + +Session discipline: the cloud endpoint is a transaction-mode pooler, so every +query runs inside one transaction whose settings are ``SET LOCAL`` only (the +translated plan carries them). The safe path additionally makes its +transaction ``READ ONLY`` — server-side write protection, with the base's +``is_cypher_safe`` regex and the layer's semantic firewall as defence-in-depth. +""" + +import re +from typing import Any, Dict, List, Optional, Tuple + +import psycopg2 +from psycopg2 import sql + +from rocketlib import warning +from ai.common.graph import GraphGlobalBase +from ai.common.graph.age import ( + AgeTranslationError, + FirewallConfig, + TranslateMode, + TranslatedQuery, + decode_agtype, + decode_row, + translate, +) +from ai.common.rocketride_db import resolve_rocketride_dsn + +DEFAULT_GRAPH_NAME = 'rocketride' +DEFAULT_MAX_ROWS = 1000 +DEFAULT_QUERY_TIMEOUT_MS = 30000 + +# Reflection fan-out bounds (mirrors graph_falkordb's sampling posture). +SCHEMA_LABEL_LIMIT = 200 +SCHEMA_REL_LIMIT = 100 + +# Same identifier rule the translation layer applies to graph names — labels +# interpolated into Cypher text must conform (see _sample_edge_endpoints). +VALID_LABEL = re.compile(r'^[A-Za-z_][A-Za-z0-9_]{0,62}$') + + +class IGlobal(GraphGlobalBase): + """Postgres + Apache AGE global for the RocketRide cloud graph node.""" + + client = None + graph_name: str = DEFAULT_GRAPH_NAME + max_rows: int = DEFAULT_MAX_ROWS + query_timeout_ms: int = DEFAULT_QUERY_TIMEOUT_MS + age_version: str = '' + + # ------------------------------------------------------------------ + # Driver lifecycle + # ------------------------------------------------------------------ + + def _open_driver(self, config: Dict[str, Any]) -> None: + """Resolve the tenant DSN, connect, and fail fast on a missing graph.""" + self.graph_name = str(config.get('graph') or DEFAULT_GRAPH_NAME).strip() + self.max_rows = self._config_int(config, 'max_rows', DEFAULT_MAX_ROWS) + self.query_timeout_ms = self._config_int(config, 'query_timeout_ms', DEFAULT_QUERY_TIMEOUT_MS) + + # The one RocketRide difference: DSN from the account seam, no config. + dsn = resolve_rocketride_dsn() + # Bounded connect so an unreachable pooler fails fast (matches the + # probe's 3s timeout) instead of waiting out the OS TCP timeout. + self.client = psycopg2.connect(dsn, connect_timeout=3) + + # Fail-fast round-trip: AGE present + the tenant graph exists. + with self.client.cursor() as cur: + cur.execute("SELECT extversion FROM pg_extension WHERE extname = 'age'") + row = cur.fetchone() + if row is None: + raise RuntimeError('The tenant database does not have the Apache AGE extension installed') + self.age_version = row[0] + cur.execute('SELECT 1 FROM ag_catalog.ag_graph WHERE name = %s', (self.graph_name,)) + graph_exists = cur.fetchone() is not None + self.client.commit() + + if not graph_exists: + # OPEN QUESTION (create_graph ownership, see the node README and + # the design's open-questions): whether the node or the cloud + # provisioner creates the per-tenant graph is undecided. Until it + # is, the node refuses to run against a missing graph rather than + # silently creating one. + raise RuntimeError( + f'AGE graph {self.graph_name!r} does not exist in the tenant database. ' + 'Graph provisioning ownership is pending; create the graph ' + f"(SELECT create_graph('{self.graph_name}')) or configure an existing one." + ) + + def _close_driver(self) -> None: + if self.client is not None: + self.client.close() + self.client = None + + def _probe_connection(self, config: Dict[str, Any]) -> None: + """Save-time probe: warn-only (the user is still editing the node).""" + conn = None + try: + dsn = resolve_rocketride_dsn() + conn = psycopg2.connect(dsn, connect_timeout=3) + with conn.cursor() as cur: + cur.execute('SELECT 1') + cur.execute("SELECT 1 FROM pg_extension WHERE extname = 'age'") + if cur.fetchone() is None: + warning('The tenant database does not have the Apache AGE extension installed') + return + graph = str(config.get('graph') or DEFAULT_GRAPH_NAME).strip() + cur.execute('SELECT 1 FROM ag_catalog.ag_graph WHERE name = %s', (graph,)) + if cur.fetchone() is None: + warning(f'AGE graph {graph!r} does not exist yet in the tenant database') + except NotImplementedError as e: + # The OSS account stub: no cloud sign-in available. + warning(str(e)) + except Exception as e: + warning(str(e).strip().splitlines()[0] if str(e).strip() else repr(e)) + finally: + try: + if conn is not None: + conn.close() + except Exception: + pass + + # ------------------------------------------------------------------ + # Query paths — everything routes through the translation layer + # ------------------------------------------------------------------ + + @property + def read_row_cap(self) -> int: + """Node-owner row ceiling for the safe read path.""" + return self.max_rows + + def _firewall_config(self) -> FirewallConfig: + return FirewallConfig(statement_timeout_ms=self.query_timeout_ms) + + def _translate( + self, + query: str, + params: Optional[Dict[str, Any]] = None, + limit: Optional[int] = None, + mode: TranslateMode = TranslateMode.SAFE, + ) -> TranslatedQuery: + return translate( + query, + params=params, + limit=limit, + mode=mode, + graph_name=self.graph_name, + age_version=self.age_version or '1.5.0', + firewall=self._firewall_config(), + ) + + def _execute_plan(self, plan: TranslatedQuery, fetch_cap: Optional[int] = None) -> List[Tuple]: + """Run one translated plan in its own transaction; return raw rows. + + The plan's READ ONLY flag is applied with ``SET TRANSACTION READ + ONLY`` as the first in-transaction statement (psycopg2 has already + opened the transaction), giving server-side write protection on the + safe path. + """ + if self.client is None: + raise RuntimeError('Database connection is not initialized') + # Both server-side controls below (SET TRANSACTION READ ONLY and the + # plan's SET LOCAL statement_timeout) are silent no-ops outside a + # transaction. They hold because psycopg2 opens one implicitly on the + # first execute — which requires autocommit off. Assert the + # precondition so a future change fails loudly instead of quietly + # dropping the write protection and the timeout cap. + if self.client.autocommit: + raise RuntimeError( + 'rocketride_graph requires autocommit=False: READ ONLY and SET LOCAL ' + 'statement_timeout only apply inside a transaction' + ) + rows: List[Tuple] = [] + try: + with self.client.cursor() as cur: + if plan.read_only: + cur.execute('SET TRANSACTION READ ONLY') + for i, (stmt, binds) in enumerate(zip(plan.statements, plan.binds)): + cur.execute(stmt, binds or None) + if i == plan.result_index and cur.description is not None: + rows = cur.fetchmany(fetch_cap) if fetch_cap else cur.fetchall() + self.client.commit() + except Exception: + self.client.rollback() + raise + return rows + + def _run_query(self, query: str, params: Optional[Dict] = None, limit: Optional[int] = None) -> List[Dict]: + """Read-only path: translate (safe mode) and run in a READ ONLY txn.""" + cap = self.read_row_cap if limit is None else min(int(limit), self.read_row_cap) + 1 + plan = self._translate(query, params=params, limit=cap, mode=TranslateMode.SAFE) + raw_rows = self._execute_plan(plan, fetch_cap=cap) + return [decode_row(plan, row) for row in raw_rows] + + def _run_query_raw(self, query: str) -> Dict[str, Any]: + """EXECUTE path: translation still applies (AGE can't run bare Cypher); + only the semantic firewall is skipped, never the resource caps. + """ + plan = self._translate(query, mode=TranslateMode.RAW) + max_rows = self.max_execute_rows + raw_rows = self._execute_plan(plan, fetch_cap=max_rows + 1) + if len(raw_rows) > max_rows: + raise ValueError(f'EXECUTE query exceeded max_execute_rows={max_rows}') + rows = [decode_row(plan, row) for row in raw_rows] + # AGE exposes no write counters (unlike Bolt); affected_rows reports + # the result-row count — 0 for writes without a RETURN clause. + return {'rows': rows, 'affected_rows': len(rows)} + + def _validate_query(self, query: str) -> Tuple[bool, str]: + """EXPLAIN the translated statement without executing it. + + Translation errors (syntax, firewall, capability) surface here too, so + the LLM repair loop receives the layer's actionable messages. + """ + try: + plan = self._translate(query, mode=TranslateMode.SAFE) + except AgeTranslationError as e: + return False, str(e) + if self.client is None: + return False, 'Database connection is not initialized' + try: + with self.client.cursor() as cur: + cur.execute('SET TRANSACTION READ ONLY') + for i, (stmt, binds) in enumerate(zip(plan.statements, plan.binds)): + if i == plan.result_index: + cur.execute('EXPLAIN ' + stmt, binds or None) + break + cur.execute(stmt, binds or None) + self.client.rollback() + return True, '' + except Exception as e: + self.client.rollback() + return False, str(e) + + # ------------------------------------------------------------------ + # Schema reflection — AGE catalog + bounded sampling + # ------------------------------------------------------------------ + + def _reflect_schema(self) -> Dict[str, Any]: + """Labels from ``ag_catalog.ag_label``; property types by sampling. + + Best-effort by contract: any failure warns and degrades to a partial + (or empty) schema rather than blocking startup. + """ + schema: Dict[str, Any] = {'nodes': {}, 'relationships': []} + if self.client is None: + return schema + + try: + with self.client.cursor() as cur: + cur.execute( + 'SELECT l.name, l.kind FROM ag_catalog.ag_label l ' + 'JOIN ag_catalog.ag_graph g ON l.graph = g.graphid ' + "WHERE g.name = %s AND l.name NOT LIKE '\\_ag\\_%%'", + (self.graph_name,), + ) + labels = cur.fetchall() + self.client.commit() + except Exception as e: + self.client.rollback() + warning(f'rocketride_graph: unable to list graph labels: {e}') + return schema + + vertex_labels = [name for name, kind in labels if kind == 'v'][:SCHEMA_LABEL_LIMIT] + edge_labels = [name for name, kind in labels if kind == 'e'][:SCHEMA_REL_LIMIT] + + for label in vertex_labels: + schema['nodes'][label] = self._sample_properties(label) + + for etype in edge_labels: + start, end = self._sample_edge_endpoints(etype) + schema['relationships'].append({'type': etype, 'start': start, 'end': end}) + + return schema + + def _sample_properties(self, label: str) -> List[Tuple[str, str]]: + """One-row property sample from the label's backing table.""" + try: + with self.client.cursor() as cur: + cur.execute(sql.SQL('SELECT properties FROM {} LIMIT 1').format(sql.Identifier(self.graph_name, label))) + row = cur.fetchone() + self.client.commit() + except Exception as e: + self.client.rollback() + warning(f'rocketride_graph: unable to sample label {label!r}: {e}') + return [] + if row is None: + return [] + properties = decode_agtype(row[0]) or {} + return [(key, type(value).__name__) for key, value in properties.items()] + + def _sample_edge_endpoints(self, etype: str) -> Tuple[str, str]: + """Start/end labels for one sampled edge of the given type.""" + # The one Cypher string the node assembles itself, so hold the label + # to the same identifier rule the layer applies to graph names: AGE + # allows backtick-quoted labels with arbitrary characters, and those + # must not be spliced into query text. + if not VALID_LABEL.fullmatch(etype or ''): + warning(f'rocketride_graph: skipping relationship sample for non-identifier label {etype!r}') + return '', '' + try: + plan = self._translate( + f'MATCH (a)-[r:{etype}]->(b) RETURN label(a) AS s, label(b) AS e', + limit=1, + mode=TranslateMode.SAFE, + ) + raw = self._execute_plan(plan, fetch_cap=1) + except Exception as e: + warning(f'rocketride_graph: unable to sample relationship {etype!r}: {e}') + return '', '' + if not raw: + return '', '' + row = decode_row(plan, raw[0]) + return str(row.get('s') or ''), str(row.get('e') or '') + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _config_int(config: Dict[str, Any], key: str, default: int) -> int: + try: + return max(1, int(config.get(key, default))) + except (TypeError, ValueError): + return default diff --git a/nodes/src/nodes/rocketride_graph/IInstance.py b/nodes/src/nodes/rocketride_graph/IInstance.py new file mode 100644 index 000000000..b23ec35c4 --- /dev/null +++ b/nodes/src/nodes/rocketride_graph/IInstance.py @@ -0,0 +1,25 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# ============================================================================= + +from ai.common.graph import GraphInstanceBase +from .IGlobal import IGlobal + + +class IInstance(GraphInstanceBase): + """RocketRide cloud graph instance. + + All tool methods (get_data, get_schema, get_query, execute, dialect) and + lane handlers are inherited from GraphInstanceBase; the Cypher->AGE + translation lives entirely behind IGlobal's query hooks. The tool surface + speaks Cypher to callers, so _query_language stays the base default. + """ + + IGlobal: IGlobal + + def _db_display_name(self) -> str: + return 'RocketRide Graph' + + def _db_dialect(self) -> str: + return 'age' diff --git a/nodes/src/nodes/rocketride_graph/README.md b/nodes/src/nodes/rocketride_graph/README.md new file mode 100644 index 000000000..d8ce1700d --- /dev/null +++ b/nodes/src/nodes/rocketride_graph/README.md @@ -0,0 +1,45 @@ +# rocketride_graph + +A RocketRide-managed graph database node backed by PostgreSQL + Apache AGE in your own provisioned RocketRide cloud database — with **zero database setup**. + +## What it does + +Mirrors the `graph_neo4j` node: as a pipeline node it takes natural-language questions on the `questions` lane, asks a connected LLM to translate them to Cypher, executes, and emits results; as a tool node agents call `get_data`, `get_schema`, `get_query`, `execute`, and `dialect` (dialect: `age`). + +Two differences from the generic graph nodes: + +1. **No connection fields.** The per-tenant DSN is resolved from the account layer (`Account.resolve_db_dsn(client_id)`), keyed by the authenticated connection identity — the same seam as `rocketride_sql` and `rocketride_vector` (one database per tenant backs all three). Requires signing into RocketRide cloud; the open-source build without a cloud identity fails with `RocketRide cloud DB nodes require signing into RocketRide cloud`. +2. **Cypher → AGE translation.** Apache AGE cannot run bare Cypher, so every query path routes through the translation layer at `ai.common.graph.age` (openCypher ANTLR parse → firewall → dialect capability gate → `cypher()` envelope with synthesized column list → prepared-statement parameter binding → agtype decode). Even the raw EXECUTE path translates — only the *semantic* firewall is skipped there, never the resource caps. + +## Safety model + +- **Safe path** (LLM/tool reads): runs in a server-side **READ ONLY transaction** (writes are refused by Postgres itself), plus the layer's semantic firewall (no write clauses, no CALL) and the base's `is_cypher_safe` regex as defence-in-depth. +- **Resource caps** (both paths): query length limit, variable-length traversal depth cap (unbounded `*` patterns are rejected), and a per-transaction `statement_timeout`. +- **EXECUTE** is gated by `allow_execute` (default off); isolation for raw writes is the database-per-tenant boundary. +- All per-query settings are `SET LOCAL` — the cloud endpoint is a transaction-mode pooler, so session-level `SET` would bleed across tenants. AGE is preloaded server-side (no `LOAD`). + +## Graph provisioning (open) + +Ownership of per-tenant `create_graph` is **pending** (cloud provisioner vs node). Until decided, the node fails fast at pipeline start when the configured graph does not exist rather than creating one silently. + +## Configuration + +### Fields + +| Field | Type | Description | +|---|---|---| +| `graph` | string | Default "rocketride". Name of the AGE graph to query | +| `db_description` | string | Default empty. What the graph contains; improves LLM query quality | +| `max_attempts` | integer | Default 5. LLM re-ask ceiling when validation rejects generated Cypher | +| `max_rows` | integer | Default 1000. Row ceiling for the read path | +| `query_timeout_ms` | integer | Default 30000. Per-transaction statement timeout | +| `allow_execute` | boolean | Default false. Enables the raw EXECUTE path | + +There are intentionally no `host` / `user` / `password` / `database` fields. + +### Dialect notes (AGE 1.5.0) + +The capability table (see `ai.common.graph.age.capabilities`) rejects constructs the cloud's AGE 1.5.0 cannot run with actionable messages: `datetime()` (store ISO-8601 strings or epoch numbers), `RETURN *` (list columns explicitly), `ORDER BY` on a projection alias (order by the expression), `MERGE ... ON CREATE/MATCH SET` (plain `MERGE` then a separate `SET`), label predicates in `WHERE` (put the label in the `MATCH` pattern), multi-labels like `(n:A:B)` (model the second label as a property or category-node edge), and `shortestPath()` (use a bounded variable-length match). All cells are empirically verified against the exact cloud pin — none pass through unverified. + + + diff --git a/nodes/src/nodes/rocketride_graph/__init__.py b/nodes/src/nodes/rocketride_graph/__init__.py new file mode 100644 index 000000000..28e1eed0e --- /dev/null +++ b/nodes/src/nodes/rocketride_graph/__init__.py @@ -0,0 +1,38 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +# ------------------------------------------------------------------------------ +# Main module +# ------------------------------------------------------------------------------ +import os +from depends import depends # type: ignore + +# Install psycopg2-binary before importing any driver code (this node drives +# psycopg2 directly; it does not use SQLAlchemy). +requirements = os.path.dirname(os.path.realpath(__file__)) + '/requirements.txt' +depends(requirements) + +from .IGlobal import IGlobal # noqa: E402 +from .IInstance import IInstance # noqa: E402 + +__all__ = ['IGlobal', 'IInstance'] diff --git a/nodes/src/nodes/rocketride_graph/requirements.txt b/nodes/src/nodes/rocketride_graph/requirements.txt new file mode 100644 index 000000000..27e38fe13 --- /dev/null +++ b/nodes/src/nodes/rocketride_graph/requirements.txt @@ -0,0 +1 @@ +psycopg2-binary==2.9.12 diff --git a/nodes/src/nodes/rocketride_graph/rocketride_graph.svg b/nodes/src/nodes/rocketride_graph/rocketride_graph.svg new file mode 100644 index 000000000..ecfcb48da --- /dev/null +++ b/nodes/src/nodes/rocketride_graph/rocketride_graph.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/nodes/src/nodes/rocketride_graph/services.json b/nodes/src/nodes/rocketride_graph/services.json new file mode 100644 index 000000000..389da5fe8 --- /dev/null +++ b/nodes/src/nodes/rocketride_graph/services.json @@ -0,0 +1,173 @@ +{ + // + // Required: + // The displayable name of this node + // + "title": "RocketRide Graph", + // + // Required: + // The protocol is the endpoint protocol + // + "protocol": "rocketride_graph://", + // + // Required: + // Class type of the node - what it does + // + "classType": ["graph", "tool"], + // + // Required: + // Capabilities are flags that change the behavior of the underlying + // engine + // + "capabilities": ["noremote", "invoke"], + // + // Optional: + // Register is either filter, endpoint or ignored if not specified. If the + // type is specified, a factory is registered of that given type + // + "register": "filter", + // + // Optional: + // The node is the actual physical node to instantiate - if + // not specified, the protocol will be used + // + "node": "python", + // + // Optional: + // The path is the executable/script code - it is node dependent + // and is optional for most node + // + "path": "nodes.rocketride_graph", + // + // Required: + // The prefix map when added/removed when converting URLs <=> paths + // + "prefix": "rocketridegraph", + // + // Optional: + // Description of this driver + // + "description": ["A RocketRide-managed graph database backed by PostgreSQL + Apache AGE. ", "Answers natural-language questions over your own provisioned RocketRide cloud ", "graph with zero setup — no host, user, or password to enter; the connection is ", "resolved automatically from your signed-in RocketRide identity. Cypher queries ", "are translated to AGE with a resource firewall, and safe reads run in read-only ", "transactions server-side."], + // + // Optional: + // The icon is the icon to display in the UI for this node + // + "icon": "rocketride_graph.svg", + "documentation": "https://docs.rocketride.org", + // + // Optional: + // Defines the invoke connections that may/must be connected + // which are utilized by this driver + // + "invoke": { + "llm": { + "description": "LLM to use to craft Cypher queries from question", + "min": 1 + } + }, + // + // Optional: + // As a pipe component, define what this pipe component takes + // and what it produces + // + "lanes": { + "questions": ["table", "text", "answers"] + }, + // + // Optional: + // Profile section are configuration options used by the driver + // itself + // + "preconfig": { + // Define the values that will be merged into any profile configuration + // specified, unless the profile is 'absolute' + "default": "default", + // Defines profiles used with the "profile": key + "profiles": { + "default": { + "graph": "rocketride" + } + } + }, + // + // Optional: + // Local fields definitions - these define fields only for the + // current service. You may specify them here, or directly + // in the shape + // + "fields": { + "rocketridegraph.graph": { + "type": "string", + "title": "Graph name", + "default": "rocketride", + "description": "Name of the AGE graph to query in your RocketRide cloud database" + }, + "rocketridegraph.db_description": { + "type": "string", + "title": "Graph description", + "default": "", + "description": "What is this graph used for? Describe its content and purpose, this helps the LLM generate more accurate queries.", + "ui": { + "ui:widget": "textarea" + } + }, + "rocketridegraph.max_attempts": { + "type": "integer", + "title": "Max validation attempts", + "default": 5, + "minimum": 1, + "maximum": 20, + "description": "Maximum number of times to re-ask the LLM if validation rejects the generated Cypher" + }, + "rocketridegraph.max_rows": { + "type": "integer", + "title": "Max read rows", + "default": 1000, + "minimum": 1, + "maximum": 25000, + "description": "Row ceiling for read queries on the questions lane and get_data tool" + }, + "rocketridegraph.query_timeout_ms": { + "type": "integer", + "title": "Query timeout (ms)", + "default": 30000, + "minimum": 100, + "maximum": 600000, + "description": "Per-query statement timeout applied inside each query's transaction" + }, + "rocketridegraph.allow_execute": { + "type": "boolean", + "title": "Allow direct query execution", + "default": false, + "description": "Permit QuestionType.EXECUTE callers to run raw Cypher without LLM translation or read-only safety checks. Resource limits still apply. Leave OFF unless a trusted application explicitly needs to issue Cypher directly." + }, + "rocketridegraph.default": { + "object": "default", + "properties": ["rocketridegraph.db_description", "rocketridegraph.graph", "rocketridegraph.max_attempts", "rocketridegraph.max_rows", "rocketridegraph.query_timeout_ms", "rocketridegraph.allow_execute"] + }, + "rocketridegraph.profile": { + "hidden": true, + "type": "string", + "default": "default", + "enum": [["default", "Default"]], + "conditional": [ + { + "value": "default", + "properties": ["rocketridegraph.default"] + } + ] + } + }, + // + // Required: + // Defines the fields (shape) of the service. Either source or target + // may be specified, or both, but at least one is required + // + "shape": [ + { + "section": "Pipe", + "title": "RocketRide Graph", + "properties": ["rocketridegraph.profile"] + } + ] +} diff --git a/nodes/src/nodes/rocketride_sql/IGlobal.py b/nodes/src/nodes/rocketride_sql/IGlobal.py new file mode 100644 index 000000000..b5b35ac72 --- /dev/null +++ b/nodes/src/nodes/rocketride_sql/IGlobal.py @@ -0,0 +1,88 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +from typing import Any, Dict + +from ai.common.database import DatabaseGlobalBase +from ai.common.rocketride_db import ( + parse_dsn_fields, + resolve_rocketride_dsn, + to_sqlalchemy_url, +) + + +class IGlobal(DatabaseGlobalBase): + """RocketRide cloud SQL global state. + + Identical to the generic PostgreSQL node in every respect except one: it + takes **no** connection config. Instead of reading host/user/password, it + resolves a ready per-tenant DSN from the account layer, keyed by the + authenticated ``client_id`` (see ``ai.common.rocketride_db``). Everything + else — schema reflection, EXPLAIN validation, the structured query surface, + and the raw EXECUTE path — is inherited unchanged from the base. + """ + + # Cache the resolved DSN between the paired _connection_params / + # _build_connection_url calls the base makes in beginGlobal and + # validateConfig, so both come from a single resolution. + _dsn: str = '' + + def _connection_params(self, config: Dict[str, Any]) -> Dict[str, str]: + """Resolve the cloud DSN and expose the pieces the base needs. + + There are no host/user/password fields; the DSN comes from + ``Account.resolve_db_dsn(client_id)``. Resolve it once here, cache it + for ``_build_connection_url``, and parse out ``database`` so the base's + schema-reflection messaging names the real tenant database. ``table`` + remains the one structured, user-facing target field. + """ + self._dsn = resolve_rocketride_dsn() + fields = parse_dsn_fields(self._dsn) + return { + 'host': fields['host'], + 'user': fields['user'], + 'password': fields['password'], + 'database': fields['database'] or 'rocketride', + 'table': config.get('table', 'table').strip(), + } + + def _build_connection_url(self, params: Dict[str, str]) -> str: + """Return the SQLAlchemy engine URL for the per-tenant cloud DSN. + + The DSN was resolved and cached in ``_connection_params`` (the base + always calls that first). Fall back to a fresh resolution if invoked + directly, so the method is safe in isolation. + """ + dsn = self._dsn or resolve_rocketride_dsn() + return to_sqlalchemy_url(dsn) + + def _max_validation_attempts(self, config: Dict[str, Any]) -> int: + """Return the EXPLAIN-validation retry count from config (default 5).""" + try: + return int(config.get('max_attempts', 5)) + except (ValueError, TypeError): + return 5 + + def _db_description(self, config: Dict[str, Any]) -> str: + """Return the user-provided database description (empty string if unset).""" + return config.get('db_description', '') diff --git a/nodes/src/nodes/rocketride_sql/IInstance.py b/nodes/src/nodes/rocketride_sql/IInstance.py new file mode 100644 index 000000000..3190cd28b --- /dev/null +++ b/nodes/src/nodes/rocketride_sql/IInstance.py @@ -0,0 +1,24 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# ============================================================================= + +from ai.common.database import DatabaseInstanceBase +from .IGlobal import IGlobal + + +class IInstance(DatabaseInstanceBase): + """RocketRide cloud SQL instance. + + All tool methods and lane handlers — including the raw EXECUTE path — are + inherited from DatabaseInstanceBase. The only per-driver knowledge is the + display name and dialect (the tenant database is PostgreSQL). + """ + + IGlobal: IGlobal + + def _db_display_name(self) -> str: + return 'RocketRide SQL' + + def _db_dialect(self) -> str: + return 'postgres' diff --git a/nodes/src/nodes/rocketride_sql/README.md b/nodes/src/nodes/rocketride_sql/README.md new file mode 100644 index 000000000..2964c883f --- /dev/null +++ b/nodes/src/nodes/rocketride_sql/README.md @@ -0,0 +1,53 @@ +# rocketride_sql + +A RocketRide-managed database node that answers natural-language questions against your own provisioned RocketRide cloud database and inserts structured pipeline data into tables — with **zero database setup**. + +## What it does + +The same two roles as the generic `db_postgres` node. As a pipeline node, it receives natural-language questions on the `questions` lane, asks a connected LLM to translate them into SQL, executes the query, and emits the results; it also accepts structured data on the `answers` lane and inserts it into the configured table. As a tool node, agents call it directly through `get_data`, `get_schema`, `get_sql`, `execute`, and `dialect`. + +The defining difference: **there are no connection fields**. Instead of host/user/password, the node resolves a ready per-tenant DSN from the account layer (`Account.resolve_db_dsn(client_id)`), keyed by the authenticated connection identity. The RocketRide cloud provisions one database per tenant; the same database backs `rocketride_sql`, `rocketride_vector`, and `rocketride_graph`, so raw SQL over the vector tables also goes through this node. + +Requires signing into RocketRide cloud. On the open-source build without a cloud identity the node fails at start with `RocketRide cloud DB nodes require signing into RocketRide cloud`. + +Safety defaults match `db_postgres`: only `SELECT` statements are permitted for LLM-generated queries, generated SQL is validated with `EXPLAIN` before execution, and raw SQL execution (`QuestionType.EXECUTE`) is disabled by default via `allow_execute`. Isolation for raw execution comes from the database-per-tenant boundary, not query inspection. + +--- + +## Connections + +| Connection | Required | Description | +| ---------- | -------- | ---------------------------------------------- | +| `llm` | yes | LLM used to generate SQL from natural language | + +--- + +## Configuration + +### Lanes + +| Lane in | Lane out | Description | +| ----------- | --------- | -------------------------------------------------------------- | +| `questions` | `table` | Translate question to SQL, execute, return as a markdown table | +| `questions` | `text` | Translate question to SQL, execute, return as text | +| `questions` | `answers` | Translate question to SQL, execute, return as answers | +| `answers` | (none) | Parse structured rows and insert into the table | + +Two special question types are handled on the `questions` lane: + +- **`QuestionType.DIALECT`**: emits `{"dialect": "postgres"}` on the `answers` lane so SDK callers can branch on the underlying engine. +- **`QuestionType.EXECUTE`**: runs the question text as raw SQL (read or write, no LLM, no safety check). Gated by `allow_execute`; when disabled the request is logged and dropped. `SELECT` results are capped at 25,000 rows; write statements report `affected_rows`. + +### Fields + +| Field | Type | Description | +|---|---|---| +| `table` | string | Default "table". Name of the table to read from or write to | +| `db_description` | string | Default empty. What is this database used for? Helps the LLM generate more accurate queries. | +| `max_attempts` | integer | Default 5. Maximum number of times to re-ask the LLM if EXPLAIN rejects the generated SQL | +| `allow_execute` | boolean | Default false. Permit QuestionType.EXECUTE callers to run raw SQL without LLM translation or safety checks. | + +There are intentionally no `host` / `user` / `password` / `database` fields — the connection is resolved from your signed-in RocketRide identity. + + + diff --git a/nodes/src/nodes/rocketride_sql/__init__.py b/nodes/src/nodes/rocketride_sql/__init__.py new file mode 100644 index 000000000..de809fdfe --- /dev/null +++ b/nodes/src/nodes/rocketride_sql/__init__.py @@ -0,0 +1,39 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +# ------------------------------------------------------------------------------ +# Main module +# ------------------------------------------------------------------------------ +import os +from depends import depends # type: ignore + +# Install psycopg2-binary before importing any driver code. SQLAlchemy is +# provided by ai.common's own depends() (SQLAlchemy>=2.0,<2.1), the same way +# the other DatabaseGlobalBase nodes get it. +requirements = os.path.dirname(os.path.realpath(__file__)) + '/requirements.txt' +depends(requirements) + +from .IGlobal import IGlobal # noqa: E402 +from .IInstance import IInstance # noqa: E402 + +__all__ = ['IGlobal', 'IInstance'] diff --git a/nodes/src/nodes/rocketride_sql/requirements.txt b/nodes/src/nodes/rocketride_sql/requirements.txt new file mode 100644 index 000000000..27e38fe13 --- /dev/null +++ b/nodes/src/nodes/rocketride_sql/requirements.txt @@ -0,0 +1 @@ +psycopg2-binary==2.9.12 diff --git a/nodes/src/nodes/rocketride_sql/rocketride_sql.svg b/nodes/src/nodes/rocketride_sql/rocketride_sql.svg new file mode 100644 index 000000000..ecfcb48da --- /dev/null +++ b/nodes/src/nodes/rocketride_sql/rocketride_sql.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/nodes/src/nodes/rocketride_sql/services.json b/nodes/src/nodes/rocketride_sql/services.json new file mode 100644 index 000000000..830ff45dc --- /dev/null +++ b/nodes/src/nodes/rocketride_sql/services.json @@ -0,0 +1,156 @@ +{ + // + // Required: + // The displayable name of this node + // + "title": "RocketRide SQL", + // + // Required: + // The protocol is the endpoint protocol + // + "protocol": "rocketride_sql://", + // + // Required: + // Class type of the node - what it does + // + "classType": ["database", "tool"], + // + // Required: + // Capabilities are flags that change the behavior of the underlying + // engine + // + "capabilities": ["noremote", "invoke"], + // + // Optional: + // Register is either filter, endpoint or ignored if not specified. If the + // type is specified, a factory is registered of that given type + // + "register": "filter", + // + // Optional: + // The node is the actual physical node to instantiate - if + // not specified, the protocol will be used + // + "node": "python", + // + // Optional: + // The path is the executable/script code - it is node dependent + // and is optional for most node + // + "path": "nodes.rocketride_sql", + // + // Required: + // The prefix map when added/removed when converting URLs <=> paths + // + "prefix": "rocketridesql", + // + // Optional: + // Description of this driver + // + "description": ["A RocketRide-managed relational database. Stores structured table data ", "in your own provisioned RocketRide cloud database with zero setup — no host, ", "user, or password to enter. The connection is resolved automatically from ", "your signed-in RocketRide identity. Supports the same natural-language query, ", "schema reflection, and direct-execute surface as the generic PostgreSQL node."], + // + // Optional: + // The icon is the icon to display in the UI for this node + // + "icon": "rocketride_sql.svg", + "documentation": "https://docs.rocketride.org", + // + // Optional: + // Defines the invoke connections that may/must be connected + // which are utilized by this driver + // + "invoke": { + "llm": { + "description": "LLM to use to craft SQL queries from question", + "min": 1 + } + }, + // + // Optional: + // As a pipe component, define what this pipe component takes + // and what it produces + // + "lanes": { + "answers": [], + "questions": ["table", "text", "answers"] + }, + // + // Optional: + // Profile section are configuration options used by the driver + // itself + // + "preconfig": { + // Define the values that will be merged into any profile configuration + // specified, unless the profile is 'absolute' + "default": "default", + // Defines profiles used with the "profile": key + "profiles": { + "default": {} + } + }, + // + // Optional: + // Local fields definitions - these define fields only for the + // current service. You may specify them here, or directly + // in the shape + // + "fields": { + "rocketridesql.table": { + "type": "string", + "title": "Table name", + "default": "table", + "description": "Name of the table to read from or write to in your RocketRide cloud database" + }, + "rocketridesql.db_description": { + "type": "string", + "title": "Database description", + "default": "", + "description": "What is this database used for? Describe its content and purpose, this helps the LLM generate more accurate queries.", + "ui": { + "ui:widget": "textarea" + } + }, + "rocketridesql.max_attempts": { + "type": "integer", + "title": "Max validation attempts", + "default": 5, + "minimum": 1, + "maximum": 20, + "description": "Maximum number of times to re-ask the LLM if EXPLAIN rejects the generated SQL" + }, + "rocketridesql.allow_execute": { + "type": "boolean", + "title": "Allow direct query execution", + "default": false, + "description": "Permit QuestionType.EXECUTE callers to run raw SQL without LLM translation or safety checks. Leave OFF unless a trusted application explicitly needs to issue SQL directly." + }, + "rocketridesql.default": { + "object": "default", + "properties": ["rocketridesql.db_description", "rocketridesql.table", "rocketridesql.max_attempts", "rocketridesql.allow_execute"] + }, + "rocketridesql.profile": { + "hidden": true, + "type": "string", + "default": "default", + "enum": [["default", "Default"]], + "conditional": [ + { + "value": "default", + "properties": ["rocketridesql.default"] + } + ] + } + }, + // + // Required: + // Defines the fields (shape) of the service. Either source or target + // may be specified, or both, but at least one is required + // + "shape": [ + { + "section": "Pipe", + "title": "RocketRide SQL", + "properties": ["rocketridesql.profile"] + } + ] +} diff --git a/nodes/src/nodes/rocketride_vector/IEndpoint.py b/nodes/src/nodes/rocketride_vector/IEndpoint.py new file mode 100644 index 000000000..d12a71f54 --- /dev/null +++ b/nodes/src/nodes/rocketride_vector/IEndpoint.py @@ -0,0 +1,28 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +from ai.common.transform import IEndpointTransform + + +class IEndpoint(IEndpointTransform): + pass diff --git a/nodes/src/nodes/rocketride_vector/IGlobal.py b/nodes/src/nodes/rocketride_vector/IGlobal.py new file mode 100644 index 000000000..b9bb0db92 --- /dev/null +++ b/nodes/src/nodes/rocketride_vector/IGlobal.py @@ -0,0 +1,157 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +# ------------------------------------------------------------------------------ +# This class controls the data shared between all threads for the task. +# +# Mirrors vectordb_postgres/IGlobal, differing only in how it connects: there +# are no host/user/password fields; the per-tenant DSN is resolved from the +# account layer (keyed by the authenticated client_id). +# ------------------------------------------------------------------------------ +import re +from rocketlib import OPEN_MODE, warning +from ai.common.transform import IGlobalTransform +from ai.common.config import Config +from ai.common.rocketride_db import resolve_rocketride_dsn + +# Valid unquoted PostgreSQL identifier: letter/underscore start, alphanumeric/underscore body, max 63 chars +VALID_TABLE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]{0,62}$') + + +class IGlobal(IGlobalTransform): + def beginGlobal(self): + # Are we in config mode or some other mode? + if self.IEndpoint.endpoint.openMode == OPEN_MODE.CONFIG: + # We are going to get a call to configureService but + # we don't actually need to load the driver for that + pass + else: + # Import store definition - even though + from .rocketride_vector import Store + + # Declare store + self.store: Store | None = None + + # Get our bag + bag = self.IEndpoint.endpoint.bag + + # Get the passed configuration + connConfig = self.getConnConfig() + + # Get the configuration + self.store = Store(self.glb.logicalType, connConfig, bag) + + # Get the info about our store + collection = self.store.collection + + # Scope the subKey by tenant: the resolved DSN points at the + # per-tenant database, so key on that database — the store must + # not be shared across tenants even when the pooler host/port are + # identical. (Identity rides the task file since #1686; env + # identity is untrusted, and the tenant DB is the actual + # isolation unit.) + tenant_scope = self.store.database or f'{self.store.host}:{self.store.port}' + subKey = f'{tenant_scope}/{collection}' + + # Call the base + super().beginGlobal(subKey) + return + + def validateConfig(self): + """ + Validate the RocketRide cloud vector store config with fast, read-only probes. + + - Enforce a safe unquoted identifier for the table (collection). + - Resolve the per-tenant DSN, connect with a short timeout, run SELECT 1. + - Verify pgvector availability via ``SELECT NULL::vector``. + - Surface raw provider errors without truncation. + """ + try: + cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig) + # Support legacy key 'collection' if the UI still sends it + table = ((cfg.get('table') or cfg.get('collection')) or '').strip() + + # Table name validation: unquoted identifier rules, total ≤63 chars + if not VALID_TABLE.fullmatch(table or ''): + warning( + 'Table name must be ≤63 chars; start with letter/underscore; only letters, digits, underscore; no spaces or special characters (- . / \\ \' " ` ( ) [ ] { } , ; : * + = | & # @ % ^ ! ? ~ $)' + ) + return + + # No host/user/password fields — resolve the per-tenant DSN. + dsn = resolve_rocketride_dsn() + + # Load deps on demand: in CONFIG mode beginGlobal never imports + # .rocketride_vector (where the node's other depends() lives), so + # this is the only thing that installs the driver at save time — + # same pattern as store_postgres and the other DB nodes. + import os + + from depends import depends # type: ignore + + depends(os.path.dirname(os.path.realpath(__file__)) + '/requirements.txt') + + import psycopg2 # type: ignore + + conn = None + try: + # Connect quickly; surface auth/host/db errors + conn = psycopg2.connect(dsn, connect_timeout=3) + with conn.cursor() as cur: + # Minimal probe + cur.execute('SELECT 1') + # Explicit type check - surfaces provider error if pgvector is not installed + cur.execute('SELECT NULL::vector') + cur.fetchone() + finally: + try: + if conn is not None: + conn.close() + except Exception: + pass + except Exception as e: + warning(_format_error(e)) + + def endGlobal(self): + # Release the index and embeddings + self.store = None + + +def _format_error(e: Exception) -> str: + """Concise error string for psycopg2 exceptions. + + psycopg2 errors carry SQL fragment + caret context on subsequent lines + that are noisy in an LLM transcript, so we keep only the first line of + the message. Anything else (including the case where ``psycopg2`` is + not installed) falls back to ``str(e).strip()``. + """ + try: + from psycopg2 import OperationalError, ProgrammingError # type: ignore + import socket + + if isinstance(e, (OperationalError, ProgrammingError, socket.timeout)): + msg = str(e).strip() + return msg.splitlines()[0] if msg else msg + except Exception: + pass + return str(e).strip() diff --git a/nodes/src/nodes/rocketride_vector/IInstance.py b/nodes/src/nodes/rocketride_vector/IInstance.py new file mode 100644 index 000000000..6fb840276 --- /dev/null +++ b/nodes/src/nodes/rocketride_vector/IInstance.py @@ -0,0 +1,80 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +# ------------------------------------------------------------------------------ +# This class controls the data for each thread of the task +# ------------------------------------------------------------------------------ +from rocketlib import Entry +from typing import List +from .IGlobal import IGlobal +from ai.common.schema import Doc, Question +from ai.common.transform import IInstanceTransform + + +class IInstance(IInstanceTransform): + IGlobal: IGlobal + + def writeQuestions(self, question: Question): + """ + Take a question, performs a search, and writes the results as documents. + """ + # Dispatch to the search handler + if self.IGlobal.store is None: + raise Exception('No document store') + self.IGlobal.store.dispatchSearch(self, question) + + def writeDocuments(self, documents: List[Doc]): + """ + Take a list of documents and adds them to the vector store. + + Any chunks in the database that have the same object id will + be removed + """ + # Add the document chunks + if self.IGlobal.store is None: + raise Exception('No document store') + self.IGlobal.store.addChunks(documents) + + def renderObject(self, object: Entry): + """ + Output all the document text to the writeText lane. + """ + + def callback(text: str) -> None: + self.instance.sendText(text) + + # Check it + if self.IGlobal.store is None: + raise Exception('No document store') + + # If we do not have a vectorize flag, or we have not vectorized + # it, allow the next driver to render + if not object.hasVectorBatchId or not object.vectorBatchId: + return + + # Render the data on this object from the store and + # send it to the renderData function + self.IGlobal.store.render(objectId=object.objectId, callback=callback) + + # Stop right here + self.preventDefault() diff --git a/nodes/src/nodes/rocketride_vector/README.md b/nodes/src/nodes/rocketride_vector/README.md new file mode 100644 index 000000000..622821a96 --- /dev/null +++ b/nodes/src/nodes/rocketride_vector/README.md @@ -0,0 +1,42 @@ +# rocketride_vector + +A RocketRide-managed vector store backed by PostgreSQL + pgvector in your own provisioned RocketRide cloud database — with **zero database setup** and a real vector index from the first write. + +## What it does + +Mirrors the generic `vectordb_postgres` store node: accepts documents (with embeddings from a bound `vectorizer`) on the `documents` lane, upserts them into a pgvector table, and serves keyword and semantic search on the `questions` lane. + +Two differences from the generic node: + +1. **No connection fields.** Instead of host/user/password, the node resolves a ready per-tenant DSN from the account layer (`Account.resolve_db_dsn(client_id)`), keyed by the authenticated connection identity. Requires signing into RocketRide cloud; on the open-source build without a cloud identity the node fails at start with `RocketRide cloud DB nodes require signing into RocketRide cloud`. +2. **Default HNSW index.** The generic node creates no index, so every semantic search is a sequential scan. This node creates an HNSW index over the embedding column when the table is first created. The operator class is derived from the `similarity` config so Postgres actually uses the index (`cosine → vector_cosine_ops`, `l2 → vector_l2_ops`, `inner_product → vector_ip_ops`), with build parameters `m = 16`, `ef_construction = 64` (overridable). pgvector's HNSW supports at most 2000 dimensions; for wider vectors the index is skipped with a warning and search falls back to a sequential scan. + +There is **no direct-execute path** — vector stores are structured (search/upsert), and raw SQL over the vector tables is covered by `rocketride_sql` (same tenant database). + +Embeddings come from the separate `vectorizer` binding — not in-node. + +--- + +## Configuration + +### Lanes + +| Lane in | Lane out | Description | +| ----------- | ----------- | ---------------------------------------------------- | +| `documents` | (none) | Upsert document chunks into the vector table | +| `questions` | `documents` | Keyword / semantic search, results emitted as documents | + +### Fields + +| Field | Type | Description | +|---|---|---| +| `collection` | string | Default "rocketride". Name of the table to store vectors | +| `score` | number | Default 0.5. Minimum similarity score for a document to be returned | +| `similarity` | string | Default "cosine". One of `cosine`, `l2`, `inner_product`. Also selects the HNSW index operator class | +| `hnsw_m` | integer | Default 16. HNSW graph degree used when the index is first created | +| `hnsw_ef_construction` | integer | Default 64. HNSW build-time candidate list size used when the index is first created | + +There are intentionally no `host` / `port` / `user` / `password` / `database` fields — the connection is resolved from your signed-in RocketRide identity. The vector dimension is not configured; it is derived from the first document's embedding at write time. + + + diff --git a/nodes/src/nodes/rocketride_vector/__init__.py b/nodes/src/nodes/rocketride_vector/__init__.py new file mode 100644 index 000000000..1aa446e9f --- /dev/null +++ b/nodes/src/nodes/rocketride_vector/__init__.py @@ -0,0 +1,48 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +# ------------------------------------------------------------------------------ +# Main module +# ------------------------------------------------------------------------------ +# Import the filters +from .IEndpoint import IEndpoint +from .IGlobal import IGlobal +from .IInstance import IInstance + + +# Allow direct access to the underlying store +def getStore(): + """ + Return the RocketRide cloud vector store class. + """ + from .rocketride_vector import Store + + return Store + + +__all__ = [ + 'IEndpoint', + 'IGlobal', + 'IInstance', + 'getStore', +] diff --git a/nodes/src/nodes/rocketride_vector/requirements.txt b/nodes/src/nodes/rocketride_vector/requirements.txt new file mode 100644 index 000000000..69c8db71c --- /dev/null +++ b/nodes/src/nodes/rocketride_vector/requirements.txt @@ -0,0 +1,2 @@ +psycopg2-binary +pgvector diff --git a/nodes/src/nodes/rocketride_vector/rocketride_vector.py b/nodes/src/nodes/rocketride_vector/rocketride_vector.py new file mode 100644 index 000000000..cabe3fa66 --- /dev/null +++ b/nodes/src/nodes/rocketride_vector/rocketride_vector.py @@ -0,0 +1,620 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +# ------------------------------------------------------------------------------ +# RocketRide cloud vector store. +# +# Mirrors the generic ``vectordb_postgres`` driver in every respect except two: +# 1. Connection — instead of reading host/user/password from config, it +# resolves a ready per-tenant DSN via ``ai.common.rocketride_db`` +# (injected by the task engine, or via the account broker). +# 2. Default index — it creates an HNSW index at first write so semantic +# search uses the index instead of a sequential scan (the generic node +# creates no index at all). The operator class is derived from the +# configured ``similarity`` metric, else Postgres ignores the index. +# Everything else (schema, upsert, keyword/semantic search, render) is identical. +# ------------------------------------------------------------------------------ +import os +from depends import depends + +requirements = os.path.dirname(os.path.realpath(__file__)) + '/requirements.txt' +depends(requirements) + +from typing import List, Callable, Dict, Any, cast +import numpy as np + +import psycopg2 +from pgvector.psycopg2 import register_vector + +from rocketlib import warning +from ai.common.schema import Doc, DocFilter, DocMetadata, QuestionText +from ai.common.store import DocumentStoreBase +from ai.common.config import Config +from ai.common.rocketride_db import parse_dsn_fields, resolve_rocketride_dsn +from .IGlobal import VALID_TABLE + +# Default PostgreSQL port +DEFAULT_POSTGRES_PORT = 5432 + +# Minimum similarity score threshold for document filtering +MIN_SIMILARITY_SCORE = 0.20 + +# pgvector HNSW cannot index vectors wider than this many dimensions. +HNSW_MAX_DIMS = 2000 + +# SQL Queries +SQL_QUERIES = { + 'check_collection_exists': "SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = %s)", + # fmt: off + 'create_collection': ( + 'CREATE TABLE IF NOT EXISTS {collection} (id bigserial PRIMARY KEY, content text, objectId text, nodeId text, parent text, permissionId int, isDeleted boolean, chunkId int, isTable boolean, tableId int, vectorSize int, modelName text, embedding vector({vector_size}));' + ), + # fmt: on + 'count_documents': 'SELECT COUNT(*) FROM {collection}', + 'search_keyword': 'SELECT * FROM {collection} WHERE content LIKE %s {where_clause} LIMIT %s', + 'get_documents': 'SELECT * FROM {collection} {where_clause} LIMIT %s', + 'get_paths': 'SELECT parent, objectId FROM {collection} WHERE {where_clause} OFFSET %s LIMIT %s', + 'insert_chunk': 'INSERT INTO {collection} ({columns}) VALUES ({placeholders})', + 'delete_by_object_ids': 'DELETE FROM {collection} WHERE objectId = ANY(%s)', + 'mark_deleted': 'UPDATE {collection} SET isDeleted = TRUE WHERE objectId = ANY(%s)', + 'mark_active': 'UPDATE {collection} SET isDeleted = FALSE WHERE objectId = ANY(%s)', + 'render_document': 'SELECT content, chunkId FROM {collection} WHERE objectId = %s AND chunkId >= %s AND chunkId < %s ORDER BY chunkId', + 'semantic_search': 'SELECT *, embedding {similarity_operator} %s AS distance FROM {collection} {where_clause} ORDER BY distance LIMIT %s', + # fmt: off + 'create_hnsw_index': ( + 'CREATE INDEX IF NOT EXISTS {index_name} ON {collection} USING hnsw (embedding {opclass}) WITH (m = {m}, ef_construction = {ef_construction});' + ), + # fmt: on +} + + +class Store(DocumentStoreBase): + apikey: str | None = None + node: str = 'rocketride' + collection: str = '' + host: str = '' + port: int = 0 + vectorSize: int = 0 + renderChunkSize: int = 32 * 1024 * 1024 + similarity: str = 'Cosine' + hnsw_m: int = 16 + hnsw_ef_construction: int = 64 + client: Any | None = None + similarityDict: dict = {'cosine': '<=>', 'l2': '<->', 'inner_product': '<#>'} + # Map the query operator (set from ``similarity``) to the HNSW operator + # class. The index opclass MUST match the search operator or Postgres will + # not use the index. + opclassDict: dict = {'<=>': 'vector_cosine_ops', '<->': 'vector_l2_ops', '<#>': 'vector_ip_ops'} + + def __init__(self, provider: str, connConfig: Dict[str, Any], bag: Dict[str, Any]): + """ + Initialize the RocketRide cloud vector store. + + @param provider: The name of the provider. + @param connConfig: The connection configuration. + @param bag: A dictionary of additional parameters. + """ + # Init the base + super().__init__(provider, connConfig, bag) + + # Get our configuration + config = Config.getNodeConfig(provider, connConfig) + + # Save our parameters — validate to prevent SQL injection via .format() + self.collection = ((config.get('table') or config.get('collection')) or '').strip() + if not self.collection or not VALID_TABLE.fullmatch(self.collection): + raise ValueError( + f'Invalid collection name: {self.collection!r}. Must be a valid PostgreSQL identifier (letters, digits, underscores; max 63 chars).' + ) + + self.renderChunkSize = config.get('renderChunkSize', self.renderChunkSize) + self.threshold_search = config.get('score', 0.5) + + # check if the similarity matches the pgvector configuration options + similarity = config.get('similarity', 'cosine') + if similarity in ['cosine', 'l2', 'inner_product']: + self.similarity = self.similarityDict[similarity] + else: + raise Exception( + 'The metric you provided in the config.json does not match required postgres configurations' + ) + + # HNSW index build parameters (overridable via config), clamped to + # pgvector's supported ranges: m in [2, 100], ef_construction in + # [4, 1000] and >= 2 * m — out-of-range values error at CREATE INDEX. + try: + self.hnsw_m = min(100, max(2, int(config.get('hnsw_m', 16)))) + except (TypeError, ValueError): + self.hnsw_m = 16 + try: + self.hnsw_ef_construction = min(1000, max(4, int(config.get('hnsw_ef_construction', 64)))) + except (TypeError, ValueError): + self.hnsw_ef_construction = 64 + self.hnsw_ef_construction = max(self.hnsw_ef_construction, 2 * self.hnsw_m) + + # RocketRide cloud: no host/user/password fields. Resolve the + # per-tenant DSN (task-engine injected, or via the account broker) and + # connect directly. host/port/database are parsed only for bookkeeping + # (the connection subKey keys on database); the DSN is the source of + # truth. + dsn = resolve_rocketride_dsn() + fields = parse_dsn_fields(dsn) + self.host = fields['host'] + self.port = fields['port'] or DEFAULT_POSTGRES_PORT + self.database = fields['database'] + + # Bounded connect so an unreachable pooler fails fast (matches the + # 3s timeout IGlobal.validateConfig uses) instead of the OS TCP timeout. + self.client = psycopg2.connect(dsn, connect_timeout=3) + + register_vector(self.client) + + return + + def __del__(self): + """ + Deinitializes the postgres client. + """ + # Deinit everything we did + self.collection = '' + self.renderChunkSize = 0 + self.similarity = 'cosine' + + # Close the client to omit memory leaks + if self.client is not None: + self.client.close() + self.client = None + + self.vectorSize = 0 + + def _doesCollectionExist(self) -> bool: + """ + Check if the collection exists. + + @returns: True if the collection exists, False otherwise. + """ + with self.client.cursor() as cur: + cur.execute(SQL_QUERIES['check_collection_exists'], (self.collection,)) + return cur.fetchone()[0] + + def _createCollection(self, vectorSize: int = 0) -> None: + """ + Create a collection and its default HNSW index. + + Unlike the generic ``vectordb_postgres`` driver (which creates no index, + leaving every semantic search as a sequential scan), this creates an + HNSW index over the embedding column at first write. The operator class + is derived from the configured similarity metric so Postgres actually + uses the index. HNSW cannot index vectors wider than ``HNSW_MAX_DIMS`` + dimensions, so for very wide vectors we skip the index and warn. + + @param vectorSize: The size of the vector. + """ + with self.client.cursor() as cur: + cur.execute(SQL_QUERIES['create_collection'].format(collection=self.collection, vector_size=vectorSize)) + + if vectorSize > HNSW_MAX_DIMS: + warning( + f'pgvector HNSW supports at most {HNSW_MAX_DIMS} dimensions; got {vectorSize} for table ' + f'{self.collection!r}. Skipping index creation — semantic search will use a sequential scan.' + ) + else: + # opclass comes from a fixed map; m / ef_construction are + # int-coerced in __init__; collection is validated against + # VALID_TABLE — all safe to interpolate into the DDL. + opclass = self.opclassDict.get(self.similarity, 'vector_cosine_ops') + cur.execute( + SQL_QUERIES['create_hnsw_index'].format( + index_name=f'{self.collection}_embedding_hnsw', + collection=self.collection, + opclass=opclass, + m=self.hnsw_m, + ef_construction=self.hnsw_ef_construction, + ) + ) + + self.client.commit() + + def _convertFilter(self, docFilter: DocFilter) -> tuple[str, list]: + """ + Build the generic filter based on required permissions, node, parent, etc. + + @param docFilter: The document filter. + @returns: A tuple containing the WHERE clause string and a list of parameters. + """ + where_clauses = [] + params = [] + + if docFilter.nodeId is not None: + where_clauses.append('nodeId = %s') + params.append(docFilter.nodeId) + + if docFilter.isTable is not None: + where_clauses.append('isTable = %s') + params.append(docFilter.isTable) + + if docFilter.tableIds is not None: + where_clauses.append('tableId = ANY(%s)') + params.append(list(docFilter.tableIds)) + + if docFilter.parent is not None: + where_clauses.append('parent = %s') + params.append(docFilter.parent) + + if docFilter.permissions is not None: + where_clauses.append('permissionId = ANY(%s)') + params.append(list(docFilter.permissions)) + + if docFilter.objectIds is not None: + where_clauses.append('objectId = ANY(%s)') + params.append(list(docFilter.objectIds)) + + if docFilter.isDeleted is None or not docFilter.isDeleted: + where_clauses.append('isDeleted = %s') + params.append(False) + + if docFilter.chunkIds is not None: + where_clauses.append('chunkId = ANY(%s)') + params.append(list(docFilter.chunkIds)) + + if docFilter.minChunkId is not None: + where_clauses.append('chunkId >= %s') + params.append(docFilter.minChunkId) + + if docFilter.maxChunkId is not None: + where_clauses.append('chunkId <= %s') + params.append(docFilter.maxChunkId) + + if not where_clauses: + return '', [] + + where_sql = ' AND '.join(where_clauses) + return 'WHERE ' + where_sql, params + + def _convertToDocs(self, points: List[Any]) -> List[Doc]: + """ + Convert a list of points or records to a docGroup. + + @param points: A list of points or records. + @returns: A list of documents. + """ + docs: List[Doc] = [] + + # Now, add the documents to the results + for point in points: + # Get the payload content and metadata + metadata_dict = {k: v for k, v in point.items() if k not in {'content', 'embedding', 'distance'}} + + key_map = { + 'objectid': 'objectId', + 'nodeid': 'nodeId', + 'permissionid': 'permissionId', + 'isdeleted': 'isDeleted', + 'chunkid': 'chunkId', + 'istable': 'isTable', + 'tableid': 'tableId', + 'modelname': 'modelName', + 'vectorsize': 'vectorSize', + } + + renamed_metadata = {key_map.get(k, k): v for k, v in metadata_dict.items()} + + # Create a new dictionary with only the expected keys + expected_keys = [ + 'objectId', + 'nodeId', + 'parent', + 'permissionId', + 'isDeleted', + 'chunkId', + 'isTable', + 'tableId', + 'modelName', + 'vectorSize', + 'id', + ] + filtered_metadata = {k: v for k, v in renamed_metadata.items() if k in expected_keys} + + # Ensure modelName and vectorSize have default values if not present or None + if 'modelName' not in filtered_metadata or filtered_metadata['modelName'] is None: + filtered_metadata['modelName'] = '' + if 'vectorSize' not in filtered_metadata or filtered_metadata['vectorSize'] is None: + filtered_metadata['vectorSize'] = 0 + + # Ensure other fields have default values if not present or None + if 'nodeId' not in filtered_metadata or filtered_metadata['nodeId'] is None: + filtered_metadata['nodeId'] = '' + if 'parent' not in filtered_metadata or filtered_metadata['parent'] is None: + filtered_metadata['parent'] = '' + if 'permissionId' not in filtered_metadata or filtered_metadata['permissionId'] is None: + filtered_metadata['permissionId'] = 0 + if 'isTable' not in filtered_metadata or filtered_metadata['isTable'] is None: + filtered_metadata['isTable'] = False + if 'tableId' not in filtered_metadata or filtered_metadata['tableId'] is None: + filtered_metadata['tableId'] = 0 + + metadata = cast(DocMetadata, filtered_metadata) + content = point.get('content') + + score = 0.0 + if 'distance' in point and point['distance'] is not None: + distance = point['distance'] + if self.similarity == '<=>': # cosine + score = 1 - distance + elif self.similarity == '<->': # l2 + score = 1 / (1 + distance) + elif self.similarity == '<#>': # inner product + score = -distance + else: + score = 1 - distance + + # Ignore it if it doesn't have a high enough score + if score < MIN_SIMILARITY_SCORE: + continue + else: + score = 0.0 + + # Create a new document + doc = Doc(score=score, page_content=content, metadata=metadata) + + # Append it to this documents chunks + docs.append(doc) + + # Return it + return docs + + def count_documents(self) -> int: + """ + Return the number of vectors in the document store, not the number of documents themselves. + + @returns: The number of documents in the document store. + """ + if not self._doesCollectionExist(): + return 0 + + with self.client.cursor() as cur: + cur.execute(SQL_QUERIES['count_documents'].format(collection=self.collection)) + return cur.fetchone()[0] + + def searchKeyword(self, query: QuestionText, docFilter: DocFilter) -> List[Doc]: + """ + Perform a keyword search. + + @param query: The query to search for. + @param docFilter: The document filter. + @returns: A list of documents that match the query. + """ + if not self._doesCollectionExist(): + return [] + + where_sql, params = self._convertFilter(docFilter) + query_params = [f'%{query}%'] + params + + with self.client.cursor() as cur: + where_clause = ('AND ' + where_sql.split('WHERE ')[1]) if where_sql else '' + cur.execute( + SQL_QUERIES['search_keyword'].format(collection=self.collection, where_clause=where_clause), + query_params + [docFilter.limit], + ) + results = cur.fetchall() + + # Convert results to dictionary + points = [dict(zip([desc[0] for desc in cur.description], row)) for row in results] + return self._convertToDocs(points) + + def get(self, docFilter: DocFilter, checkCollection: bool = True) -> List[Doc]: + """ + Retrieve document groups matching a given filter. + + @param docFilter: The document filter. + @param checkCollection: Whether to check if the collection exists. + @returns: A list of documents that match the filter. + """ + if checkCollection and not self._doesCollectionExist(): + return [] + + where_sql, params = self._convertFilter(docFilter) + + with self.client.cursor() as cur: + cur.execute( + SQL_QUERIES['get_documents'].format(collection=self.collection, where_clause=where_sql), + params + [docFilter.limit], + ) + results = cur.fetchall() + + points = [dict(zip([desc[0] for desc in cur.description], row)) for row in results] + return self._convertToDocs(points) + + def getPaths(self, parent: str | None = None, offset: int = 0, limit: int = 1000) -> Dict[str, str]: + """ + Retrieve unique parent paths. + + @param parent: The parent path. + @param offset: The offset to start from. + @param limit: The maximum number of paths to return. + @returns: A dictionary of parent paths and their object IDs. + """ + if not self._doesCollectionExist(): + return {} + + where_clauses = ['chunkId = 0'] + params = [] + + if parent is not None: + where_clauses.append('parent = %s') + params.append(parent) + + where_sql = ' AND '.join(where_clauses) + + with self.client.cursor() as cur: + cur.execute( + SQL_QUERIES['get_paths'].format(collection=self.collection, where_clause=where_sql), + params + [offset, limit], + ) + results = cur.fetchall() + + paths = {row[0]: row[1] for row in results} + return paths + + def addChunks(self, chunks: List[Doc], checkCollection: bool = True) -> None: + """ + Add document chunks to the document store. + + @param chunks: A list of documents to add. + @param checkCollection: Whether to check if the collection exists. + """ + if not chunks: + return + + if checkCollection and not self.createCollection(chunks): + return + + objectIds = {chunk.metadata.objectId for chunk in chunks} + self.remove(list(objectIds)) + + with self.client.cursor() as cur: + for chunk in chunks: + data = {'content': chunk.page_content, **chunk.metadata.model_dump(), 'embedding': chunk.embedding} + if 'vectorSize' not in data or data['vectorSize'] is None: + data['vectorSize'] = len(chunk.embedding) if chunk.embedding else 0 + if 'modelName' not in data or data['modelName'] is None: + data['modelName'] = '' + columns = ', '.join(data.keys()) + placeholders = ', '.join(['%s'] * len(data)) + cur.execute( + SQL_QUERIES['insert_chunk'].format( + collection=self.collection, columns=columns, placeholders=placeholders + ), + list(data.values()), + ) + self.client.commit() + + def remove(self, objectIds: List[str]) -> None: + """ + Delete all documents with matching objectIds from the document store. + + @param objectIds: A list of object IDs to delete. + """ + if not self._doesCollectionExist(): + return + + with self.client.cursor() as cur: + cur.execute(SQL_QUERIES['delete_by_object_ids'].format(collection=self.collection), (objectIds,)) + self.client.commit() + + def markDeleted(self, objectIds: List[str]) -> None: + """ + Mark the set of documents with the given objectId as deleted. + + @param objectIds: A list of object IDs to mark as deleted. + """ + if not self._doesCollectionExist(): + return + + with self.client.cursor() as cur: + cur.execute(SQL_QUERIES['mark_deleted'].format(collection=self.collection), (objectIds,)) + self.client.commit() + + def markActive(self, objectIds: List[str]) -> None: + """ + Mark the set of documents with the given objectId as active. + + @param objectIds: A list of object IDs to mark as active. + """ + if not self._doesCollectionExist(): + return + + with self.client.cursor() as cur: + cur.execute(SQL_QUERIES['mark_active'].format(collection=self.collection), (objectIds,)) + self.client.commit() + + def render(self, objectId: str, callback: Callable[[str], None]) -> None: + """ + Given an object id, renders the complete document. + + @param objectId: The object ID to render. + @param callback: The callback function to call with the rendered text. + """ + if not self._doesCollectionExist(): + return + + offset = 0 + while True: + with self.client.cursor() as cur: + cur.execute( + SQL_QUERIES['render_document'].format(collection=self.collection), + (objectId, offset, offset + self.renderChunkSize), + ) + results = cur.fetchall() + + if not results: + break + + # Assemble by chunk index, bounded by the rows actually fetched — + # a single row with a high chunkId must not size a buffer. + content_by_index = {chunkId - offset: content for content, chunkId in results if chunkId >= offset} + if not content_by_index: + break + + fullText = ''.join(content_by_index[index] for index in sorted(content_by_index)) + callback(fullText) + + # NB: len(results) is a row count while renderChunkSize is a + # chunkId window width — different units (inherited from + # store_postgres). With the default window everything fits in one + # pass, so this exit is effectively always taken; the real fix + # belongs in the shared store base (dedup follow-up PR). + if len(results) < self.renderChunkSize: + break + + offset += self.renderChunkSize + + def searchSemantic(self, query: QuestionText, docFilter: DocFilter) -> List[Doc]: + """ + Perform a semantic search. + + @param query: The query to search for. + @param docFilter: The document filter. + @returns: A list of documents that match the query. + """ + if not self._doesCollectionExist(): + return [] + + if query.embedding is None: + raise Exception('To use semantic search, you must bind to an embedding module') + + where_sql, params = self._convertFilter(docFilter) + embedding_arr = np.array(query.embedding) + + with self.client.cursor() as cur: + cur.execute( + SQL_QUERIES['semantic_search'].format( + collection=self.collection, similarity_operator=self.similarity, where_clause=where_sql + ), + (embedding_arr, *params, docFilter.limit), + ) + results = cur.fetchall() + + # Convert results to dictionary + points = [dict(zip([desc[0] for desc in cur.description], row)) for row in results] + return self._convertToDocs(points) diff --git a/nodes/src/nodes/rocketride_vector/rocketride_vector.svg b/nodes/src/nodes/rocketride_vector/rocketride_vector.svg new file mode 100644 index 000000000..ecfcb48da --- /dev/null +++ b/nodes/src/nodes/rocketride_vector/rocketride_vector.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/nodes/src/nodes/rocketride_vector/services.json b/nodes/src/nodes/rocketride_vector/services.json new file mode 100644 index 000000000..8be267f5b --- /dev/null +++ b/nodes/src/nodes/rocketride_vector/services.json @@ -0,0 +1,193 @@ +{ + // + // Required: + // The displayable name of this node + // + "title": "RocketRide Vector", + // + // Required: + // The protocol is the endpoint protocol + // + "protocol": "rocketride_vector://", + // + // Required: + // Class type of the node - what it does + // + "classType": ["store"], + // + // Required: + // Capabilities are flags that change the behavior of the underlying + // engine + // + "capabilities": [], + // + // Optional: + // Register is either filter, endpoint or ignored if not specified. If the + // type is specified, a factory is registered of that given type + // + "register": "filter", + // + // Optional: + // The node is the actual physical node to instantiate - if + // not specified, the protocol will be used + // + "node": "python", + // + // Optional: + // The path is the executable/script code - it is node dependent + // and is optional for most node + // + "path": "nodes.rocketride_vector", + // + // Required: + // The prefix map when added/removed when converting URLs <=> paths + // + "prefix": "RocketRide Vector Store", + // + // Optional: + // Description to of this driver + // + "description": ["A RocketRide-managed vector store backed by PostgreSQL + pgvector. Stores ", "and semantically searches high-dimensional embeddings in your own provisioned ", "RocketRide cloud database with zero setup — no host, user, or password to enter; ", "the connection is resolved automatically from your signed-in RocketRide identity. ", "An HNSW index is created on first write so semantic search is fast from the start."], + // + // Optional: + // The icon is the icon to display in the UI for this node + // + "icon": "rocketride_vector.svg", + "documentation": "https://docs.rocketride.org", + // + // Optional: + // Rendering hints to the UI which indicate which fields of + // the configuration should be used to display information + // + "tile": ["Table: ${parameters.rocketride_vector.${profile}.collection}", "Similarity: ${parameters.rocketride_vector.${profile}.similarity}"], + // + // Optional: + // As a pipe component, define what this pipe component takes + // and what it produces + // + "lanes": { + "documents": [], + "questions": ["documents", "answers", "questions"] + }, + // + // Optional: + // Profile section are configuration options used by the driver + // itself + // + "preconfig": { + "default": "cloud", + "profiles": { + "cloud": { + "mode": "cloud", + "title": "RocketRide cloud", + "collection": "rocketride", + "score": 0.5, + "similarity": "cosine", + "hnsw_m": 16, + "hnsw_ef_construction": 64 + } + } + }, + // + // Optional: + // Local fields definitions - these define fields only for the + // current service. You may specify them here, or directly + // in the shape + // + "fields": { + "rrvector.collection": { + "title": "Table", + "description": "Name of the table to store vectors in your RocketRide cloud database.", + "type": "string", + "default": "rocketride" + }, + "rrvector.score": { + "title": "Score threshold", + "description": "Minimum similarity score for a document to be returned by search.", + "type": "number", + "default": 0.5 + }, + "rrvector.similarity": { + "title": "Similarity Metric", + "description": "The similarity metric to use for vector search. Also selects the HNSW index operator class.", + "type": "string", + "default": "cosine", + "enum": ["cosine", "l2", "inner_product"] + }, + "rrvector.hnsw_m": { + "title": "HNSW m", + "description": "HNSW graph degree (max connections per layer) used when the index is first created.", + "type": "integer", + "default": 16, + "minimum": 2, + "maximum": 100 + }, + "rrvector.hnsw_ef_construction": { + "title": "HNSW ef_construction", + "description": "HNSW build-time candidate list size used when the index is first created.", + "type": "integer", + "default": 64, + "minimum": 4, + "maximum": 1000 + }, + "rrvector.cloud": { + "object": "cloud", + "properties": ["rrvector.collection", "rrvector.score", "rrvector.similarity", "rrvector.hnsw_m", "rrvector.hnsw_ef_construction"] + }, + "rrvector.profile": { + "hidden": true, + "title": "RocketRide cloud database", + "description": "Connect to...", + "type": "string", + "default": "cloud", + "enum": ["*>preconfig.profiles.*.title"], + "conditional": [ + { + "value": "cloud", + "properties": ["rrvector.cloud"] + } + ] + }, + "rrvector.provider": { + "type": "string", + "const": "rocketride_vector", + "default": "rocketride_vector" + }, + "rrvector.rocketride_vector": { + "object": "rocketride_vector", + "properties": ["rrvector.profile"] + }, + "rrvector.store": { + "object": "store", + "title": "Store", + "properties": ["rrvector.provider", "rrvector.rocketride_vector"] + }, + "rrvector.target.autopipe": { + "object": "parameters", + "title": "Parameters", + "properties": ["remote", "rrvector.store", "all.embedding"] + }, + "rrvector.target.parameters": { + "object": "parameters", + "title": "Parameters", + "properties": ["rrvector.target.autopipe"] + } + }, + // + // Required: + // Defines the fields (shape) of the service. Either source or target + // map be specified, or both, but at least one is required + // + "shape": [ + { + "section": "Pipe", + "title": "RocketRide Vector Store", + "properties": ["rrvector.profile"] + }, + { + "section": "Transform", + "title": "RocketRide Vector", + "properties": ["type", "rrvector.target.parameters"] + } + ] +} diff --git a/nodes/test/test_age_translate.py b/nodes/test/test_age_translate.py new file mode 100644 index 000000000..a1ec77a84 --- /dev/null +++ b/nodes/test/test_age_translate.py @@ -0,0 +1,359 @@ +# ============================================================================= +# RocketRide Engine +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Unit tests for the Cypher -> Apache AGE translation layer. + +The layer is a pure transform (no database), so everything here runs offline. +It is loaded in isolation from disk (no ``ai.__init__`` chain) — its only +external dependency is the antlr4 runtime. +""" + +from __future__ import annotations + +import importlib.util +import sys + +from pathlib import Path + +import pytest + +pytest.importorskip('antlr4') + +_AGE_DIR = Path(__file__).resolve().parents[2] / 'packages' / 'ai' / 'src' / 'ai' / 'common' / 'graph' / 'age' + + +def _load_age(): + name = '_test_rr_age' + spec = importlib.util.spec_from_file_location( + name, _AGE_DIR / '__init__.py', submodule_search_locations=[str(_AGE_DIR)] + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +age = _load_age() +SAFE = age.TranslateMode.SAFE +RAW = age.TranslateMode.RAW + + +# --------------------------------------------------------------------------- +# analysis +# --------------------------------------------------------------------------- + + +class TestAnalyze: + def test_return_aliases_and_expressions(self): + facts = age.analyze('MATCH (a) RETURN a.name AS name, count(a) AS cnt, a.x') + assert [c.display_name for c in facts.return_columns] == ['name', 'cnt', 'a.x'] + + def test_backticked_alias_stripped(self): + facts = age.analyze('MATCH (n) RETURN n.a AS `weird name`') + assert facts.return_columns[0].display_name == 'weird name' + + def test_with_projection_not_mistaken_for_return(self): + facts = age.analyze('MATCH (n) WITH n.a AS x, n.b AS y RETURN x') + assert [c.display_name for c in facts.return_columns] == ['x'] + + def test_union_uses_first_branch(self): + facts = age.analyze('MATCH (a) RETURN a.x UNION MATCH (b) RETURN b.y') + assert [c.display_name for c in facts.return_columns] == ['a.x'] + + def test_no_return_statement(self): + facts = age.analyze("CREATE (n:P {name: 'x'})") + assert facts.return_columns is None + assert facts.write_clauses == {'CREATE'} + assert facts.is_write + + def test_return_star_detected(self): + assert age.analyze('MATCH (n) RETURN *').returns_star + + def test_write_clauses_detected_as_typed_contexts(self): + facts = age.analyze('MATCH (n) SET n.a = 1 REMOVE n.b DELETE n') + assert facts.write_clauses == {'SET', 'REMOVE', 'DELETE'} + + def test_merge_action_flag(self): + facts = age.analyze('MERGE (n:P {k: 1}) ON CREATE SET n.v = 1 RETURN n') + assert facts.has_merge_action + # A SET outside MERGE does not raise the flag. + assert not age.analyze('MATCH (n) SET n.a = 1').has_merge_action + + def test_var_length_ranges(self): + facts = age.analyze('MATCH (a)-[*2..5]->(b)-[*3]->(c)-[*..4]->(d)-[*]->(e) RETURN a') + assert facts.var_length_ranges == [(2, 5), (3, 3), (None, 4), (None, None)] + + def test_params_and_functions(self): + facts = age.analyze('MATCH (n) WHERE n.a = $x AND n.b = $y RETURN toUpper(n.c)') + assert facts.param_names == {'x', 'y'} + assert facts.function_names == {'toupper'} + + def test_multi_label_and_where_label_check(self): + assert age.analyze('MATCH (n:A:B) RETURN n').has_multi_label + facts = age.analyze('MATCH (n) WHERE (n:Label) RETURN n') + assert facts.has_where_label_check + # Labels inside a MATCH pattern are not WHERE label checks. + assert not age.analyze('MATCH (n:Label) RETURN n').has_where_label_check + + def test_syntax_error_raises_translation_error(self): + with pytest.raises(age.AgeTranslationError, match='syntax error'): + age.analyze('MATCHX (n RETURN n') + + def test_empty_query_raises(self): + with pytest.raises(age.AgeTranslationError, match='Empty'): + age.analyze(' ') + + +# --------------------------------------------------------------------------- +# firewall +# --------------------------------------------------------------------------- + + +class TestFirewall: + def test_write_rejected_on_safe_path_only(self): + with pytest.raises(age.AgeFirewallRejected, match='write_clause'): + age.translate('CREATE (n)', mode=SAFE, graph_name='g') + # Same statement passes on the raw path. + assert age.translate('CREATE (n)', mode=RAW, graph_name='g').has_return is False + + def test_call_rejected_on_safe_path(self): + with pytest.raises(age.AgeFirewallRejected, match='procedure_call'): + age.translate('CALL db.labels()', mode=SAFE, graph_name='g') + + def test_unbounded_var_length_rejected_on_both_paths(self): + for mode in (SAFE, RAW): + with pytest.raises(age.AgeFirewallRejected, match='unbounded_var_length'): + age.translate('MATCH (a)-[*]->(b) RETURN a', mode=mode, graph_name='g') + + def test_depth_cap_applies_to_raw_path_too(self): + with pytest.raises(age.AgeFirewallRejected, match='max_var_length_depth'): + age.translate('MATCH (a)-[*1..99]->(b) RETURN a', mode=RAW, graph_name='g') + + def test_depth_cap_configurable(self): + config = age.FirewallConfig(max_var_length_depth=99) + plan = age.translate('MATCH (a)-[*1..99]->(b) RETURN a', mode=SAFE, graph_name='g', firewall=config) + assert plan.columns == ['a'] + + def test_query_length_cap(self): + config = age.FirewallConfig(max_query_length=30) + with pytest.raises(age.AgeFirewallRejected, match='max_query_length'): + age.translate('MATCH (averylongvariablename) RETURN averylongvariablename', graph_name='g', firewall=config) + + def test_length_cap_checked_before_parse(self): + # An oversize query full of syntactically invalid text must produce the + # LENGTH error, not a syntax error — proving the cap precedes the parse + # (the parse is the expensive step the cap exists to bound). + config = age.FirewallConfig(max_query_length=100) + garbage = ':::not cypher at all::: ' * 50 + with pytest.raises(age.AgeFirewallRejected, match='max_query_length'): + age.translate(garbage, graph_name='g', firewall=config) + + def test_deep_nesting_rejected_pre_parse(self): + # ~14 stack frames per nesting level in the ANTLR parser; 100 levels + # used to escape as a bare RecursionError (reviewer-reproduced). + query = 'RETURN ' + '(' * 100 + '1' + ')' * 100 + with pytest.raises(age.AgeFirewallRejected, match='max_nesting_depth'): + age.translate(query, graph_name='g') + + def test_deep_nesting_backstop_is_translation_error(self): + # Even with the pre-parse cap lifted, deep nesting must surface as + # AgeTranslationError (the layer's contract), never RecursionError. + query = 'RETURN ' + '(' * 300 + '1' + ')' * 300 + config = age.FirewallConfig(max_nesting_depth=10_000) + with pytest.raises(age.AgeTranslationError, match='nested too deeply'): + age.translate(query, graph_name='g', firewall=config) + + def test_moderate_nesting_still_translates(self): + query = 'RETURN ' + '(' * 10 + '1' + ')' * 10 + plan = age.translate(query, graph_name='g') + assert plan.has_return is True + + +# --------------------------------------------------------------------------- +# capabilities +# --------------------------------------------------------------------------- + + +class TestCapabilities: + def test_datetime_rejected_on_1_5_0(self): + with pytest.raises(age.AgeUnsupportedFeature, match='datetime'): + age.translate('MATCH (n) RETURN datetime()', graph_name='g') + + def test_return_star_rejected(self): + with pytest.raises(age.AgeUnsupportedFeature, match='explicit'): + age.translate('MATCH (n) RETURN *', graph_name='g') + + def test_order_by_alias_rejected(self): + with pytest.raises(age.AgeUnsupportedFeature, match='order by the expression'): + age.translate('MATCH (a)-[r:K]->(b) RETURN r.since AS since ORDER BY since', graph_name='g') + # Ordering by the expression itself is fine. + plan = age.translate('MATCH (a)-[r:K]->(b) RETURN r.since AS since ORDER BY r.since', graph_name='g') + assert plan.columns == ['since'] + # A bare variable that is not an alias is not flagged. + plan2 = age.translate('MATCH (n) RETURN n.name AS name ORDER BY n', graph_name='g') + assert plan2.columns == ['name'] + + def test_promoted_cells_reject_with_guidance(self): + # Verified 2026-07-28 against the exact pin (PG 16.14 + AGE 1.5.0): + # all four former-TBD constructs are syntax-level failures, so the + # layer now rejects them pre-flight with an actionable alternative. + cases = [ + ('MERGE (n:P {k: 1}) ON CREATE SET n.v = 1 RETURN n', 'separate SET'), + ('MATCH (n) WHERE (n:P) RETURN n', 'pattern instead'), + ('MATCH (n:A:B) RETURN n', 'category node'), + # NB: 'MATCH p = shortestPath(...)' is not openCypher grammar and is + # stopped even earlier, by the parser; the function-position form is + # what reaches the capability gate. + ('MATCH (a:P), (b:P) RETURN shortestPath((a)-[*..3]-(b))', 'variable-length'), + ] + for query, hint in cases: + with pytest.raises(age.AgeUnsupportedFeature, match=hint): + age.translate(query, mode=RAW, graph_name='g') + + def test_table_structure(self): + assert age.DEFAULT_AGE_VERSION == '1.5.0' + table = age.CAPABILITY_TABLES['1.5.0'] + assert table['datetime_function'].status is age.CellStatus.REJECT + # No cell remains unverified: every 1.5.0 cell has an empirical status. + tbd = {k for k, cap in table.items() if cap.status is age.CellStatus.TBD} + assert tbd == set() + for feature in ('merge_on_set', 'where_label_check', 'multi_label', 'shortest_path'): + assert table[feature].status is age.CellStatus.REJECT + + def test_unknown_version_falls_back(self): + with pytest.raises(age.AgeUnsupportedFeature): + age.translate('MATCH (n) RETURN datetime()', graph_name='g', age_version='9.9.9') + + +# --------------------------------------------------------------------------- +# emit +# --------------------------------------------------------------------------- + + +class TestEmit: + def test_envelope_shape_without_params(self): + plan = age.translate('MATCH (n:P) RETURN n.name AS name, n', graph_name='mygraph', limit=7) + select = plan.statements[plan.result_index] + assert select.startswith("SELECT * FROM cypher('mygraph', $rr_cypher$") + assert select.endswith('AS (c0 agtype, c1 agtype) LIMIT 7') + assert plan.columns == ['name', 'n'] + assert plan.read_only is True + # Session discipline: LOCAL-only settings precede the query. + assert plan.statements[0] == 'SET LOCAL search_path = ag_catalog,"$user",public' + assert plan.statements[1].startswith('SET LOCAL statement_timeout') + + def test_no_return_synthesizes_single_column(self): + plan = age.translate('CREATE (n:P)', mode=RAW, graph_name='g') + assert 'AS (v agtype)' in plan.statements[plan.result_index] + assert plan.has_return is False + + def test_params_use_prepare_execute_deallocate(self): + plan = age.translate('MATCH (n) WHERE n.a = $x RETURN n', params={'x': 1}, graph_name='g') + prepare, execute, deallocate = plan.statements[2], plan.statements[3], plan.statements[4] + assert prepare.startswith('PREPARE _rr_age_') + assert '(agtype) AS SELECT * FROM cypher(' in prepare + assert execute.startswith('EXECUTE _rr_age_') and execute.endswith('(%s::agtype)') + assert plan.binds[3] == ('{"x": 1}',) + assert deallocate.startswith('DEALLOCATE _rr_age_') + assert plan.result_index == 3 + + def test_params_without_placeholders_rejected(self): + with pytest.raises(age.AgeTranslationError, match='references no'): + age.translate('MATCH (n) RETURN n', params={'x': 1}, graph_name='g') + + def test_referenced_params_without_values_rejected(self): + with pytest.raises(age.AgeTranslationError, match=r'no supplied values: \$x'): + age.translate('MATCH (n) WHERE n.a = $x RETURN n', graph_name='g') + + def test_partially_supplied_params_rejected(self): + with pytest.raises(age.AgeTranslationError, match=r'no supplied values: \$y'): + age.translate( + 'MATCH (n) WHERE n.a = $x AND n.b = $y RETURN n', + params={'x': 1}, + graph_name='g', + ) + + def test_dollar_tag_rotates_on_collision(self): + plan = age.translate("MATCH (n) WHERE n.note = '$rr_cypher$x' RETURN n.note", graph_name='g') + assert '$rr_cypher1$' in plan.statements[plan.result_index] + + def test_invalid_graph_name_rejected(self): + with pytest.raises(age.AgeTranslationError, match='Invalid graph name'): + age.translate('MATCH (n) RETURN n', graph_name='bad-name; DROP') + + def test_unserialisable_params_rejected(self): + with pytest.raises(age.AgeTranslationError, match='JSON-serialisable'): + age.translate('MATCH (n) WHERE n.a = $x RETURN n', params={'x': object()}, graph_name='g') + + +# --------------------------------------------------------------------------- +# decode +# --------------------------------------------------------------------------- + + +class TestDecode: + def test_vertex(self): + value = age.decode_agtype('{"id": 1, "label": "P", "properties": {"a": 3, "name": "x"}}::vertex') + assert value == {'id': 1, 'label': 'P', 'properties': {'a': 3, 'name': 'x'}} + + def test_edge_carries_endpoints(self): + value = age.decode_agtype('{"id": 5, "label": "K", "end_id": 2, "start_id": 1, "properties": {"w": 2}}::edge') + assert value['start_id'] == 1 + assert value['end_id'] == 2 + assert value['properties'] == {'w': 2} + + def test_scalars_and_null(self): + # Plain equality on purpose: decode converts agtype numerics through + # Decimal -> float, which is exact for these literals, and + # pytest.approx consults sys.modules['numpy'] at call time — under the + # full xdist gate another worker test can leave a stubbed numpy there, + # making approx itself blow up. + assert age.decode_agtype('"alice"') == 'alice' + assert age.decode_agtype('3.14') == 3.14 + assert age.decode_agtype('42') == 42 + assert age.decode_agtype('true') is True + assert age.decode_agtype(None) is None + + def test_nested_containers(self): + assert age.decode_agtype('[1, {"a": [2, 3]}]') == [1, {'a': [2, 3]}] + + def test_string_escapes_decoded(self): + # agtype STRING follows the JSON string grammar; escapes must decode + # to the real characters, not survive as literal backslash sequences. + assert age.decode_agtype('"a\\nb"') == 'a\nb' + assert age.decode_agtype('"say \\"hi\\""') == 'say "hi"' + assert age.decode_agtype('"caf\\u00e9"') == 'café' + value = age.decode_agtype('{"properties": {"k\\"ey": "v1\\tv2"}}::vertex') + assert value['properties'] == {'k"ey': 'v1\tv2'} + + def test_decode_row_keys_by_display_names(self): + plan = age.translate('MATCH (n) RETURN n.name AS name, n.age', graph_name='g') + row = age.decode_row(plan, ('"alice"', '3')) + assert row == {'name': 'alice', 'n.age': 3} + + def test_bad_agtype_raises(self): + with pytest.raises(age.AgeTranslationError, match='agtype syntax error'): + age.decode_agtype('{"unterminated') diff --git a/nodes/test/test_rocketride_db.py b/nodes/test/test_rocketride_db.py new file mode 100644 index 000000000..e9cbcb8f9 --- /dev/null +++ b/nodes/test/test_rocketride_db.py @@ -0,0 +1,579 @@ +# ============================================================================= +# RocketRide Engine +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Unit tests for the RocketRide cloud DB nodes (no database required). + +Covers the shared connection seam (``ai.common.rocketride_db``), the OSS +``Account.resolve_db_dsn`` stub, the ``rocketride_sql`` connection override, +and the ``rocketride_vector`` HNSW default-index logic. Real base classes are +loaded from disk with ``rocketlib`` and heavy drivers stubbed, mirroring +``test_graph_neo4j.py``. +""" + +from __future__ import annotations + +import asyncio +import importlib.util +import sys +import types + +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parents[2] +_AI_SRC = _REPO / 'packages' / 'ai' / 'src' +_RRDB_PATH = _AI_SRC / 'ai' / 'common' / 'rocketride_db.py' +_ACCOUNT_BASE = _AI_SRC / 'ai' / 'account' / 'base.py' +_ACCOUNT_OSS = _AI_SRC / 'ai' / 'account' / 'oss' / '__init__.py' +_SQL_NODE_DIR = _REPO / 'nodes' / 'src' / 'nodes' / 'rocketride_sql' +_VEC_NODE_DIR = _REPO / 'nodes' / 'src' / 'nodes' / 'rocketride_vector' + +TEST_DSN = 'postgresql://rruser:rrpass@localhost:55432/rrtenant' + + +def _load_from_path(name: str, path: Path, *, is_package: bool = False): + search = [str(path.parent)] if is_package else None + spec = importlib.util.spec_from_file_location(name, path, submodule_search_locations=search) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +def _load_rrdb(): + """Load ai.common.rocketride_db in isolation (it only imports stdlib).""" + return _load_from_path('_test_rocketride_db_helpers', _RRDB_PATH) + + +rrdb = _load_rrdb() + + +# --------------------------------------------------------------------------- +# Shared seam helpers +# --------------------------------------------------------------------------- + + +class TestDsnHelpers: + def test_url_gets_psycopg2_driver(self): + assert rrdb.to_sqlalchemy_url(TEST_DSN) == 'postgresql+psycopg2://rruser:rrpass@localhost:55432/rrtenant' + + def test_postgres_scheme_gets_psycopg2_driver(self): + assert rrdb.to_sqlalchemy_url('postgres://u:p@h/db') == 'postgresql+psycopg2://u:p@h/db' + + def test_explicit_driver_passes_through(self): + dsn = 'postgresql+psycopg2://u:p@h/db' + assert rrdb.to_sqlalchemy_url(dsn) == dsn + + def test_keyvalue_dsn_passes_through(self): + dsn = 'host=localhost dbname=rrtenant user=rruser' + assert rrdb.to_sqlalchemy_url(dsn) == dsn + + def test_parse_dsn_fields(self): + fields = rrdb.parse_dsn_fields(TEST_DSN) + assert fields == { + 'host': 'localhost', + 'port': 55432, + 'user': 'rruser', + 'password': 'rrpass', + 'database': 'rrtenant', + } + + def test_parse_dsn_fields_unquotes_credentials(self): + fields = rrdb.parse_dsn_fields('postgresql://u%40x:p%23w@h:5432/db') + assert fields['user'] == 'u@x' + assert fields['password'] == 'p#w' + + def test_parse_keyvalue_dsn_yields_empty_fields(self): + fields = rrdb.parse_dsn_fields('host=x dbname=y') + assert fields['host'] == '' + assert fields['database'] == '' + + def test_parse_malformed_port_degrades_that_field_only(self): + fields = rrdb.parse_dsn_fields('postgresql://u:p@h:notaport/db') + assert fields['port'] is None + assert fields['host'] == 'h' + assert fields['database'] == 'db' + + def test_parse_malformed_authority_yields_empty_fields(self): + # Mismatched IPv6 bracket — urlparse itself raises ValueError. + fields = rrdb.parse_dsn_fields('postgresql://[::1/db') + assert fields == {'host': '', 'port': None, 'user': '', 'password': '', 'database': ''} + + +class TestClientId: + def test_missing_env_raises(self, monkeypatch): + monkeypatch.delenv(rrdb.CLIENT_ID_ENV, raising=False) + with pytest.raises(ValueError, match='signed-in RocketRide cloud identity'): + rrdb.current_client_id() + + def test_present_env_returned_stripped(self, monkeypatch): + monkeypatch.setenv(rrdb.CLIENT_ID_ENV, ' tenant-42 ') + assert rrdb.current_client_id() == 'tenant-42' + + +class TestResolveRocketrideDsn: + """The full seam: injected env DSN first, else account singleton via async bridge.""" + + def _install_fake_account(self, monkeypatch, resolver): + monkeypatch.delenv(rrdb.DB_DSN_ENV, raising=False) + ai_pkg = types.ModuleType('ai') + ai_pkg.__path__ = [] + account_mod = types.ModuleType('ai.account') + account_mod.account = types.SimpleNamespace(resolve_db_dsn=resolver) + monkeypatch.setitem(sys.modules, 'ai', ai_pkg) + monkeypatch.setitem(sys.modules, 'ai.account', account_mod) + + def test_injected_env_dsn_wins(self, monkeypatch): + """The task-engine-injected ROCKETRIDE_DB_DSN short-circuits everything.""" + + async def fake(client_id): # pragma: no cover — must not be reached + raise AssertionError('account must not be consulted when the env DSN is present') + + self._install_fake_account(monkeypatch, fake) + monkeypatch.setenv(rrdb.DB_DSN_ENV, f' {TEST_DSN} ') + # No client id needed on the injected path either. + monkeypatch.delenv(rrdb.CLIENT_ID_ENV, raising=False) + assert rrdb.resolve_rocketride_dsn() == TEST_DSN + + def test_resolve_error_env_surfaces_broker_failure(self, monkeypatch): + """A server-side resolution failure names the real cause, not sign-in.""" + + async def fake(client_id): # pragma: no cover — must not be reached + raise AssertionError('account must not be consulted when a resolve error is present') + + self._install_fake_account(monkeypatch, fake) + monkeypatch.delenv(rrdb.DB_DSN_ENV, raising=False) + monkeypatch.setenv(rrdb.DB_RESOLVE_ERROR_ENV, 'DB broker request failed: HTTP 503') + with pytest.raises(ValueError, match='HTTP 503'): + rrdb.resolve_rocketride_dsn() + + def test_empty_env_dsn_falls_back_to_account(self, monkeypatch): + async def fake(client_id): + return TEST_DSN + + self._install_fake_account(monkeypatch, fake) + monkeypatch.setenv(rrdb.DB_DSN_ENV, ' ') + monkeypatch.setenv(rrdb.CLIENT_ID_ENV, 'tenant-42') + assert rrdb.resolve_rocketride_dsn() == TEST_DSN + + def test_resolves_via_fake_account(self, monkeypatch): + seen = {} + + async def fake(client_id): + seen['client_id'] = client_id + return TEST_DSN + + self._install_fake_account(monkeypatch, fake) + monkeypatch.setenv(rrdb.CLIENT_ID_ENV, 'tenant-42') + assert rrdb.resolve_rocketride_dsn() == TEST_DSN + assert seen['client_id'] == 'tenant-42' + + def test_empty_dsn_raises(self, monkeypatch): + async def fake(client_id): + return '' + + self._install_fake_account(monkeypatch, fake) + monkeypatch.setenv(rrdb.CLIENT_ID_ENV, 'tenant-42') + with pytest.raises(ValueError, match='empty DSN'): + rrdb.resolve_rocketride_dsn() + + def test_rejects_running_event_loop(self, monkeypatch): + async def fake(client_id): + return TEST_DSN + + self._install_fake_account(monkeypatch, fake) + monkeypatch.setenv(rrdb.CLIENT_ID_ENV, 'tenant-42') + + async def call_from_loop(): + return rrdb.resolve_rocketride_dsn() + + with pytest.raises(RuntimeError, match='running event loop'): + asyncio.run(call_from_loop()) + + +# --------------------------------------------------------------------------- +# Account layer: OSS stub + base default +# --------------------------------------------------------------------------- + + +class TestAccountStub: + @pytest.fixture() + def account_cls(self, monkeypatch): + """Load the OSS Account with ``depends`` stubbed out.""" + depends_mod = types.ModuleType('depends') + depends_mod.depends = lambda *a, **kw: None + monkeypatch.setitem(sys.modules, 'depends', depends_mod) + + ai_pkg = types.ModuleType('ai') + ai_pkg.__path__ = [str(_AI_SRC / 'ai')] + monkeypatch.setitem(sys.modules, 'ai', ai_pkg) + account_pkg = types.ModuleType('ai.account') + account_pkg.__path__ = [str(_ACCOUNT_BASE.parent)] + monkeypatch.setitem(sys.modules, 'ai.account', account_pkg) + base_mod = _load_from_path('ai.account.base', _ACCOUNT_BASE) + monkeypatch.setitem(sys.modules, 'ai.account.base', base_mod) + oss_mod = _load_from_path('ai.account.oss', _ACCOUNT_OSS, is_package=True) + monkeypatch.setitem(sys.modules, 'ai.account.oss', oss_mod) + return base_mod.AccountBase, oss_mod.Account + + def test_oss_stub_raises_cloud_signin_message(self, account_cls): + _, account = account_cls + with pytest.raises(NotImplementedError, match='require signing into RocketRide cloud'): + asyncio.run(account().resolve_db_dsn('tenant-42')) + + def test_base_default_raises(self, account_cls): + base, _ = account_cls + + class Minimal(base): + async def authenticate(self, credential): + return None + + with pytest.raises(NotImplementedError): + asyncio.run(Minimal().resolve_db_dsn('tenant-42')) + + +# --------------------------------------------------------------------------- +# rocketride_sql: connection resolution override +# --------------------------------------------------------------------------- + + +def _sql_iglobal_cls(monkeypatch): + """Load rocketride_sql/IGlobal with the database base loaded from disk.""" + pytest.importorskip('sqlalchemy') + + rocketlib = types.ModuleType('rocketlib') + rocketlib.IGlobalBase = type('IGlobalBase', (), {}) + rocketlib.error = lambda *a, **kw: None + rocketlib.warning = lambda *a, **kw: None + monkeypatch.setitem(sys.modules, 'rocketlib', rocketlib) + + ai_pkg = types.ModuleType('ai') + ai_pkg.__path__ = [] + common_pkg = types.ModuleType('ai.common') + common_pkg.__path__ = [] + config = types.ModuleType('ai.common.config') + config.Config = types.SimpleNamespace(getNodeConfig=lambda *a, **kw: {}) + monkeypatch.setitem(sys.modules, 'ai', ai_pkg) + monkeypatch.setitem(sys.modules, 'ai.common', common_pkg) + monkeypatch.setitem(sys.modules, 'ai.common.config', config) + + rrdb_mod = _load_from_path('ai.common.rocketride_db', _RRDB_PATH) + monkeypatch.setitem(sys.modules, 'ai.common.rocketride_db', rrdb_mod) + + db_global = _load_from_path( + 'ai.common.database.db_global_base', + _AI_SRC / 'ai' / 'common' / 'database' / 'db_global_base.py', + ) + database_pkg = types.ModuleType('ai.common.database') + database_pkg.__path__ = [] + database_pkg.DatabaseGlobalBase = db_global.DatabaseGlobalBase + monkeypatch.setitem(sys.modules, 'ai.common.database', database_pkg) + monkeypatch.setitem(sys.modules, 'ai.common.database.db_global_base', db_global) + + iglobal = _load_from_path('nodes.rocketride_sql.IGlobal', _SQL_NODE_DIR / 'IGlobal.py') + return iglobal.IGlobal, rrdb_mod + + +class TestRocketrideSqlConnection: + def _with_fake_account(self, monkeypatch): + account_mod = types.ModuleType('ai.account') + + async def fake(client_id): + return TEST_DSN + + account_mod.account = types.SimpleNamespace(resolve_db_dsn=fake) + monkeypatch.setitem(sys.modules, 'ai.account', account_mod) + monkeypatch.setenv('ROCKETRIDE_CLIENT_ID', 'tenant-42') + monkeypatch.delenv('ROCKETRIDE_DB_DSN', raising=False) + + def test_connection_params_come_from_resolved_dsn(self, monkeypatch): + cls, _ = _sql_iglobal_cls(monkeypatch) + self._with_fake_account(monkeypatch) + glb = cls() + params = glb._connection_params({'table': ' widgets '}) + assert params['host'] == 'localhost' + assert params['user'] == 'rruser' + assert params['password'] == 'rrpass' + assert params['database'] == 'rrtenant' + assert params['table'] == 'widgets' + + def test_build_connection_url_uses_cached_dsn(self, monkeypatch): + cls, _ = _sql_iglobal_cls(monkeypatch) + self._with_fake_account(monkeypatch) + glb = cls() + params = glb._connection_params({}) + url = glb._build_connection_url(params) + assert url == 'postgresql+psycopg2://rruser:rrpass@localhost:55432/rrtenant' + + def test_no_connection_fields_read_from_config(self, monkeypatch): + """host/user/password in config must be ignored — the DSN wins.""" + cls, _ = _sql_iglobal_cls(monkeypatch) + self._with_fake_account(monkeypatch) + glb = cls() + params = glb._connection_params({'host': 'evil', 'user': 'evil', 'password': 'evil'}) + assert params['host'] == 'localhost' + assert params['user'] == 'rruser' + url = glb._build_connection_url(params) + assert 'evil' not in url + + +# --------------------------------------------------------------------------- +# rocketride_vector: HNSW default-index logic +# --------------------------------------------------------------------------- + + +class _FakeCursor: + def __init__(self, executed, fetch_batches): + self._executed = executed + self._fetch_batches = fetch_batches + + def execute(self, sql, params=None): + self._executed.append(sql) + + def fetchone(self): + # Existence probes (_doesCollectionExist) read a single boolean. + return (True,) + + def fetchall(self): + return self._fetch_batches.pop(0) if self._fetch_batches else [] + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + +class _FakeClient: + def __init__(self): + self.executed: list[str] = [] + self.commits = 0 + # Queue of row batches returned by successive fetchall() calls. + self.fetch_batches: list[list] = [] + + def cursor(self): + return _FakeCursor(self.executed, self.fetch_batches) + + def commit(self): + self.commits += 1 + + def close(self): + pass + + +def _vector_store_cls(monkeypatch): + """Load the rocketride_vector Store with heavy deps stubbed.""" + warnings: list[str] = [] + + depends_mod = types.ModuleType('depends') + depends_mod.depends = lambda *a, **kw: None + monkeypatch.setitem(sys.modules, 'depends', depends_mod) + + rocketlib = types.ModuleType('rocketlib') + rocketlib.warning = warnings.append + rocketlib.OPEN_MODE = types.SimpleNamespace(CONFIG=object()) + rocketlib.Entry = type('Entry', (), {}) + monkeypatch.setitem(sys.modules, 'rocketlib', rocketlib) + + numpy_mod = types.ModuleType('numpy') + numpy_mod.array = lambda x: x + monkeypatch.setitem(sys.modules, 'numpy', numpy_mod) + + psycopg2_mod = types.ModuleType('psycopg2') + psycopg2_mod.connect = lambda *a, **kw: _FakeClient() + monkeypatch.setitem(sys.modules, 'psycopg2', psycopg2_mod) + pgvector_mod = types.ModuleType('pgvector') + pgvector_psycopg2 = types.ModuleType('pgvector.psycopg2') + pgvector_psycopg2.register_vector = lambda client: None + pgvector_mod.psycopg2 = pgvector_psycopg2 + monkeypatch.setitem(sys.modules, 'pgvector', pgvector_mod) + monkeypatch.setitem(sys.modules, 'pgvector.psycopg2', pgvector_psycopg2) + + ai_pkg = types.ModuleType('ai') + ai_pkg.__path__ = [] + common_pkg = types.ModuleType('ai.common') + common_pkg.__path__ = [] + schema = types.ModuleType('ai.common.schema') + for name in ('Doc', 'DocFilter', 'DocMetadata', 'QuestionText'): + setattr(schema, name, type(name, (), {})) + store_mod = types.ModuleType('ai.common.store') + + class _StubDocumentStoreBase: + def __init__(self, provider, connConfig, bag): + self.vectorSize = 0 + self.modelName = '' + self.threshold_search = 0.5 + + store_mod.DocumentStoreBase = _StubDocumentStoreBase + config = types.ModuleType('ai.common.config') + config.Config = types.SimpleNamespace(getNodeConfig=lambda provider, connConfig: dict(connConfig)) + transform = types.ModuleType('ai.common.transform') + for name in ('IGlobalTransform', 'IInstanceTransform', 'IEndpointTransform'): + setattr(transform, name, type(name, (), {})) + monkeypatch.setitem(sys.modules, 'ai', ai_pkg) + monkeypatch.setitem(sys.modules, 'ai.common', common_pkg) + monkeypatch.setitem(sys.modules, 'ai.common.schema', schema) + monkeypatch.setitem(sys.modules, 'ai.common.store', store_mod) + monkeypatch.setitem(sys.modules, 'ai.common.config', config) + monkeypatch.setitem(sys.modules, 'ai.common.transform', transform) + + rrdb_mod = _load_from_path('ai.common.rocketride_db', _RRDB_PATH) + monkeypatch.setitem(sys.modules, 'ai.common.rocketride_db', rrdb_mod) + + account_mod = types.ModuleType('ai.account') + + async def fake(client_id): + return TEST_DSN + + account_mod.account = types.SimpleNamespace(resolve_db_dsn=fake) + monkeypatch.setitem(sys.modules, 'ai.account', account_mod) + monkeypatch.setenv('ROCKETRIDE_CLIENT_ID', 'tenant-42') + monkeypatch.delenv('ROCKETRIDE_DB_DSN', raising=False) + + iglobal = _load_from_path('nodes.rocketride_vector.IGlobal', _VEC_NODE_DIR / 'IGlobal.py') + pkg = types.ModuleType('nodes.rocketride_vector') + pkg.__path__ = [str(_VEC_NODE_DIR)] + pkg.IGlobal = iglobal + monkeypatch.setitem(sys.modules, 'nodes.rocketride_vector', pkg) + monkeypatch.setitem(sys.modules, 'nodes.rocketride_vector.IGlobal', iglobal) + store = _load_from_path('nodes.rocketride_vector.rocketride_vector', _VEC_NODE_DIR / 'rocketride_vector.py') + return store.Store, warnings + + +def _make_store(cls, config): + return cls('rocketride_vector', config, {}) + + +class TestRocketrideVectorStore: + def test_connects_via_resolved_dsn(self, monkeypatch): + cls, _ = _vector_store_cls(monkeypatch) + store = _make_store(cls, {'collection': 'rr_vec', 'similarity': 'cosine'}) + # No client_id on the store: identity rides the task file (#1686); + # tenant scoping keys on the DSN's database (see IGlobal's subKey). + assert store.host == 'localhost' + assert store.port == 55432 + assert store.database == 'rrtenant' + assert isinstance(store.client, _FakeClient) + + def test_invalid_collection_rejected(self, monkeypatch): + cls, _ = _vector_store_cls(monkeypatch) + with pytest.raises(ValueError, match='Invalid collection name'): + _make_store(cls, {'collection': 'bad-name;drop', 'similarity': 'cosine'}) + + @pytest.mark.parametrize( + ('similarity', 'opclass'), + [ + ('cosine', 'vector_cosine_ops'), + ('l2', 'vector_l2_ops'), + ('inner_product', 'vector_ip_ops'), + ], + ) + def test_hnsw_index_opclass_follows_similarity(self, monkeypatch, similarity, opclass): + cls, _ = _vector_store_cls(monkeypatch) + store = _make_store(cls, {'collection': 'rr_vec', 'similarity': similarity}) + store._createCollection(vectorSize=5) + ddl = store.client.executed + assert any('CREATE TABLE IF NOT EXISTS rr_vec' in sql for sql in ddl) + index_sql = [sql for sql in ddl if 'USING hnsw' in sql] + assert len(index_sql) == 1 + assert f'(embedding {opclass})' in index_sql[0] + assert 'rr_vec_embedding_hnsw' in index_sql[0] + assert 'm = 16' in index_sql[0] + assert 'ef_construction = 64' in index_sql[0] + + def test_hnsw_params_overridable(self, monkeypatch): + cls, _ = _vector_store_cls(monkeypatch) + store = _make_store( + cls, + {'collection': 'rr_vec', 'similarity': 'cosine', 'hnsw_m': 32, 'hnsw_ef_construction': 128}, + ) + store._createCollection(vectorSize=5) + index_sql = [sql for sql in store.client.executed if 'USING hnsw' in sql] + assert 'm = 32' in index_sql[0] + assert 'ef_construction = 128' in index_sql[0] + + def test_render_orders_chunks_and_bridges_gaps(self, monkeypatch): + """Chunks at 0, 1 and 3 (gap at 2): index order preserved, gap skipped.""" + cls, _ = _vector_store_cls(monkeypatch) + store = _make_store(cls, {'collection': 'rr_vec', 'similarity': 'cosine'}) + store.client.fetch_batches = [[('A', 0), ('D', 3), ('B', 1)]] + out: list[str] = [] + store.render('obj-1', out.append) + assert out == ['ABD'] + + def test_render_empty_batch_emits_nothing(self, monkeypatch): + cls, _ = _vector_store_cls(monkeypatch) + store = _make_store(cls, {'collection': 'rr_vec', 'similarity': 'cosine'}) + store.client.fetch_batches = [[]] + out: list[str] = [] + store.render('obj-1', out.append) + assert out == [] + + def test_render_single_high_chunk_id(self, monkeypatch): + """One row with a high chunkId renders that row — allocation is bounded + by rows fetched, never by the chunkId value. + """ + cls, _ = _vector_store_cls(monkeypatch) + store = _make_store(cls, {'collection': 'rr_vec', 'similarity': 'cosine'}) + store.client.fetch_batches = [[('X', 999_999)]] + out: list[str] = [] + store.render('obj-1', out.append) + assert out == ['X'] + + def test_hnsw_params_clamped_to_pgvector_bounds(self, monkeypatch): + cls, _ = _vector_store_cls(monkeypatch) + store = _make_store( + cls, + {'collection': 'rr_vec', 'similarity': 'cosine', 'hnsw_m': 1, 'hnsw_ef_construction': 1}, + ) + # pgvector requires m >= 2, ef_construction >= 4 and >= 2 * m. + assert store.hnsw_m == 2 + assert store.hnsw_ef_construction == 4 + + def test_hnsw_ef_construction_raised_to_twice_m(self, monkeypatch): + cls, _ = _vector_store_cls(monkeypatch) + store = _make_store( + cls, + {'collection': 'rr_vec', 'similarity': 'cosine', 'hnsw_m': 64, 'hnsw_ef_construction': 64}, + ) + assert store.hnsw_ef_construction == 128 + + def test_hnsw_skipped_above_dimension_ceiling(self, monkeypatch): + cls, warnings = _vector_store_cls(monkeypatch) + store = _make_store(cls, {'collection': 'rr_vec', 'similarity': 'cosine'}) + store._createCollection(vectorSize=2001) + ddl = store.client.executed + assert any('CREATE TABLE IF NOT EXISTS rr_vec' in sql for sql in ddl) + assert not any('USING hnsw' in sql for sql in ddl) + assert any('2000' in w and 'Skipping index' in w for w in warnings) + + def test_hnsw_created_at_dimension_ceiling(self, monkeypatch): + cls, warnings = _vector_store_cls(monkeypatch) + store = _make_store(cls, {'collection': 'rr_vec', 'similarity': 'cosine'}) + store._createCollection(vectorSize=2000) + assert any('USING hnsw' in sql for sql in store.client.executed) + assert not warnings diff --git a/nodes/test/test_rocketride_db_full.py b/nodes/test/test_rocketride_db_full.py new file mode 100644 index 000000000..7237bbcf4 --- /dev/null +++ b/nodes/test/test_rocketride_db_full.py @@ -0,0 +1,532 @@ +# ============================================================================= +# RocketRide Engine +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Integration tests for the RocketRide cloud DB nodes (real PostgreSQL). + +Runs the ``rocketride_sql`` and ``rocketride_vector`` nodes end-to-end against +a local PostgreSQL container standing in for the cloud data-core, connected +through the real seam (env ``ROCKETRIDE_CLIENT_ID`` + a fake, injectable +``Account.resolve_db_dsn`` — exactly how tests are meant to swap the resolver; +node code is identical regardless of which resolver is active). + +Requires a container on the live pin (PG16 + pgvector), e.g.:: + + docker run -d --name rr-pg-phase1 -p 55432:5432 \ + -e POSTGRES_PASSWORD=rrpass -e POSTGRES_USER=rruser -e POSTGRES_DB=rrtenant \ + pgvector/pgvector:pg16 + docker exec rr-pg-phase1 psql -U rruser -d rrtenant -c 'CREATE EXTENSION IF NOT EXISTS vector;' + +Configuration via environment variables: + RR_TEST_PG_DSN — libpq URL of the test database + (default: postgresql://rruser:rrpass@localhost:55432/rrtenant) + +Skips cleanly when the database is unreachable. +""" + +from __future__ import annotations + +import importlib.util +import os +import sys +import urllib.parse +import types + +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parents[2] +_AI_SRC = _REPO / 'packages' / 'ai' / 'src' +_RRDB_PATH = _AI_SRC / 'ai' / 'common' / 'rocketride_db.py' +_DB_BASE_DIR = _AI_SRC / 'ai' / 'common' / 'database' +_SQL_NODE_DIR = _REPO / 'nodes' / 'src' / 'nodes' / 'rocketride_sql' +_VEC_NODE_DIR = _REPO / 'nodes' / 'src' / 'nodes' / 'rocketride_vector' + +TEST_DSN = os.environ.get('RR_TEST_PG_DSN', 'postgresql://rruser:rrpass@localhost:55432/rrtenant') +# Derived, not hardcoded: the suite must also run against a provisioned tenant +# DSN (e.g. t__ through a pooler), not just the default container. +TEST_DB_NAME = urllib.parse.urlparse(TEST_DSN).path.lstrip('/') +TEST_CLIENT_ID = 'tenant-integration-test' + +# RR_REQUIRE_DB_TESTS (set by CI once its DB containers are healthy) turns +# every skip in this module into a hard failure: a broken container or missing +# dependency must never let the safety-control tests silently go green. +_DB_TESTS_REQUIRED = bool(os.environ.get('RR_REQUIRE_DB_TESTS')) + +if _DB_TESTS_REQUIRED: + import numpy as np + import pgvector # noqa: F401 + import psycopg2 + import sqlalchemy # noqa: F401 +else: + psycopg2 = pytest.importorskip('psycopg2') + pytest.importorskip('pgvector') + pytest.importorskip('sqlalchemy') + np = pytest.importorskip('numpy') + + +def _db_reachable() -> bool: + try: + conn = psycopg2.connect(TEST_DSN, connect_timeout=3) + conn.close() + return True + except Exception: + return False + + +if _DB_TESTS_REQUIRED and not _db_reachable(): + pytest.fail(f'RR_REQUIRE_DB_TESTS is set but the test database is not reachable at {TEST_DSN}', pytrace=False) + +pytestmark = pytest.mark.skipif(not _db_reachable(), reason=f'RocketRide test database not reachable at {TEST_DSN}') + + +def _load_from_path(name: str, path: Path, *, is_package: bool = False): + search = [str(path.parent)] if is_package else None + spec = importlib.util.spec_from_file_location(name, path, submodule_search_locations=search) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +# --------------------------------------------------------------------------- +# Environment: stubs + the fake resolver (the injectable test seam) +# --------------------------------------------------------------------------- + + +@pytest.fixture() +def rr_env(monkeypatch): + """Stub the engine surface and install the fake DSN resolver.""" + warnings: list[str] = [] + + depends_mod = types.ModuleType('depends') + depends_mod.depends = lambda *a, **kw: None + monkeypatch.setitem(sys.modules, 'depends', depends_mod) + + rocketlib = types.ModuleType('rocketlib') + rocketlib.IGlobalBase = type('IGlobalBase', (), {}) + rocketlib.IInstanceBase = type('IInstanceBase', (), {}) + rocketlib.tool_function = lambda **kwargs: lambda f: f + rocketlib.debug = lambda *a, **kw: None + rocketlib.error = lambda *a, **kw: None + rocketlib.warning = warnings.append + rocketlib.OPEN_MODE = types.SimpleNamespace(CONFIG=object()) + rocketlib.Entry = type('Entry', (), {}) + rocketlib_types = types.ModuleType('rocketlib.types') + rocketlib_types.IInvokeLLM = types.SimpleNamespace(Ask=lambda **kw: kw) + rocketlib.types = rocketlib_types + monkeypatch.setitem(sys.modules, 'rocketlib', rocketlib) + monkeypatch.setitem(sys.modules, 'rocketlib.types', rocketlib_types) + + ai_pkg = types.ModuleType('ai') + ai_pkg.__path__ = [] + common_pkg = types.ModuleType('ai.common') + common_pkg.__path__ = [] + + schema = types.ModuleType('ai.common.schema') + + class _Metadata: + def __init__(self, **kw): + self._data = kw + for k, v in kw.items(): + setattr(self, k, v) + + def model_dump(self): + return dict(self._data) + + class _Doc: + def __init__(self, score=0.0, page_content='', metadata=None, embedding=None, embedding_model=''): + self.score = score + self.page_content = page_content + self.metadata = _Metadata(**metadata) if isinstance(metadata, dict) else metadata + self.embedding = embedding + self.embedding_model = embedding_model + + class _DocFilter: + def __init__(self, **kw): + for name in ( + 'nodeId', + 'isTable', + 'tableIds', + 'parent', + 'permissions', + 'objectIds', + 'isDeleted', + 'chunkIds', + 'minChunkId', + 'maxChunkId', + ): + setattr(self, name, kw.get(name)) + self.limit = kw.get('limit', 10) + + class _QuestionText(str): + embedding = None + embedding_model = '' + + schema.Doc = _Doc + schema.DocFilter = _DocFilter + schema.DocMetadata = _Metadata + schema.QuestionText = _QuestionText + schema.Answer = type('Answer', (), {}) + schema.Question = type('Question', (), {}) + schema.QuestionType = types.SimpleNamespace(QUESTION=1, DIALECT=2, EXECUTE=3) + + table = types.ModuleType('ai.common.table') + table.Table = types.SimpleNamespace( + generate_markdown_table=lambda data, headers=None: '\n'.join(' | '.join(map(str, r)) for r in data) + ) + + config = types.ModuleType('ai.common.config') + config.Config = types.SimpleNamespace(getNodeConfig=lambda provider, connConfig: dict(connConfig)) + + transform = types.ModuleType('ai.common.transform') + for name in ('IGlobalTransform', 'IInstanceTransform', 'IEndpointTransform'): + setattr(transform, name, type(name, (), {})) + + store_mod = types.ModuleType('ai.common.store') + + class _StubDocumentStoreBase: + """Minimal stand-in for DocumentStoreBase's first-write flow.""" + + def __init__(self, provider, connConfig, bag): + self.vectorSize = 0 + self.modelName = '' + self.threshold_search = 0.5 + + def createCollection(self, documents) -> bool: + if not self._doesCollectionExist(): + self._createCollection(len(documents[0].embedding)) + return True + + store_mod.DocumentStoreBase = _StubDocumentStoreBase + + monkeypatch.setitem(sys.modules, 'ai', ai_pkg) + monkeypatch.setitem(sys.modules, 'ai.common', common_pkg) + monkeypatch.setitem(sys.modules, 'ai.common.schema', schema) + monkeypatch.setitem(sys.modules, 'ai.common.table', table) + monkeypatch.setitem(sys.modules, 'ai.common.config', config) + monkeypatch.setitem(sys.modules, 'ai.common.transform', transform) + monkeypatch.setitem(sys.modules, 'ai.common.store', store_mod) + + rrdb_mod = _load_from_path('ai.common.rocketride_db', _RRDB_PATH) + monkeypatch.setitem(sys.modules, 'ai.common.rocketride_db', rrdb_mod) + + # The injectable fake resolver: same seam a SaaS build fills in. + account_mod = types.ModuleType('ai.account') + + async def fake_resolve_db_dsn(client_id): + assert client_id == TEST_CLIENT_ID + return TEST_DSN + + account_mod.account = types.SimpleNamespace(resolve_db_dsn=fake_resolve_db_dsn) + monkeypatch.setitem(sys.modules, 'ai.account', account_mod) + monkeypatch.setenv('ROCKETRIDE_CLIENT_ID', TEST_CLIENT_ID) + monkeypatch.delenv('ROCKETRIDE_DB_DSN', raising=False) + + return types.SimpleNamespace(schema=schema, warnings=warnings) + + +@pytest.fixture() +def raw_conn(): + conn = psycopg2.connect(TEST_DSN) + yield conn + conn.close() + + +# --------------------------------------------------------------------------- +# rocketride_sql end-to-end +# --------------------------------------------------------------------------- + + +def _load_sql_node(monkeypatch): + db_global = _load_from_path('ai.common.database.db_global_base', _DB_BASE_DIR / 'db_global_base.py') + sql_safety = _load_from_path('ai.common.database.sql_safety', _DB_BASE_DIR / 'sql_safety.py') + database_pkg = types.ModuleType('ai.common.database') + database_pkg.__path__ = [str(_DB_BASE_DIR)] + database_pkg.DatabaseGlobalBase = db_global.DatabaseGlobalBase + monkeypatch.setitem(sys.modules, 'ai.common.database', database_pkg) + monkeypatch.setitem(sys.modules, 'ai.common.database.db_global_base', db_global) + monkeypatch.setitem(sys.modules, 'ai.common.database.sql_safety', sql_safety) + db_instance = _load_from_path('ai.common.database.db_instance_base', _DB_BASE_DIR / 'db_instance_base.py') + database_pkg.DatabaseInstanceBase = db_instance.DatabaseInstanceBase + monkeypatch.setitem(sys.modules, 'ai.common.database.db_instance_base', db_instance) + + iglobal = _load_from_path('nodes.rocketride_sql.IGlobal', _SQL_NODE_DIR / 'IGlobal.py') + pkg = types.ModuleType('nodes.rocketride_sql') + pkg.__path__ = [str(_SQL_NODE_DIR)] + pkg.IGlobal = iglobal + monkeypatch.setitem(sys.modules, 'nodes.rocketride_sql', pkg) + monkeypatch.setitem(sys.modules, 'nodes.rocketride_sql.IGlobal', iglobal) + iinstance = _load_from_path('nodes.rocketride_sql.IInstance', _SQL_NODE_DIR / 'IInstance.py') + return iglobal.IGlobal, iinstance.IInstance + + +class TestRocketrideSqlE2E: + @pytest.fixture() + def sql_table(self, raw_conn): + with raw_conn.cursor() as cur: + cur.execute('DROP TABLE IF EXISTS rr_sql_e2e') + cur.execute('CREATE TABLE rr_sql_e2e (id serial PRIMARY KEY, name text, qty int)') + cur.execute("INSERT INTO rr_sql_e2e (name, qty) VALUES ('bolt', 7), ('nut', 3)") + raw_conn.commit() + yield 'rr_sql_e2e' + with raw_conn.cursor() as cur: + cur.execute('DROP TABLE IF EXISTS rr_sql_e2e') + raw_conn.commit() + + def _begin(self, monkeypatch, config): + iglobal_cls, iinstance_cls = _load_sql_node(monkeypatch) + glb = iglobal_cls() + glb.glb = types.SimpleNamespace(logicalType='rocketride_sql', connConfig=config) + glb.beginGlobal() + inst = iinstance_cls() + inst.IGlobal = glb + return glb, inst + + def test_begin_global_connects_and_reflects(self, rr_env, monkeypatch, sql_table): + glb, _ = self._begin(monkeypatch, {'table': sql_table}) + try: + assert glb.database == TEST_DB_NAME + assert glb.table == sql_table + assert sql_table in glb.db_schema + assert 'name' in glb.schema + ok, err = glb._validateQuery(f'SELECT * FROM {sql_table}') + assert ok, err + finally: + glb.endGlobal() + + def test_execute_gated_by_allow_execute(self, rr_env, monkeypatch, sql_table): + glb, inst = self._begin(monkeypatch, {'table': sql_table, 'allow_execute': False}) + try: + with pytest.raises(ValueError, match='execute tool is disabled'): + inst.execute({'sql': f'SELECT * FROM {sql_table}'}) + finally: + glb.endGlobal() + + def test_execute_runs_real_query(self, rr_env, monkeypatch, sql_table): + glb, inst = self._begin(monkeypatch, {'table': sql_table, 'allow_execute': True}) + try: + result = inst.execute({'sql': f'SELECT name, qty FROM {sql_table} ORDER BY qty DESC'}) + assert [r['name'] for r in result['rows']] == ['bolt', 'nut'] + written = inst.execute({'sql': f"INSERT INTO {sql_table} (name, qty) VALUES ('washer', 11)"}) + assert written['affected_rows'] == 1 + back = inst.execute({'sql': f'SELECT count(*) AS n FROM {sql_table}'}) + assert back['rows'][0]['n'] == 3 + finally: + glb.endGlobal() + + def test_begin_global_via_injected_env_dsn(self, rr_env, monkeypatch, sql_table): + """The production delivery path: the task engine resolves server-side and + injects ROCKETRIDE_DB_DSN; the node-side account is never consulted. + """ + import types as _types + + async def poisoned(client_id): # pragma: no cover — must not be reached + raise AssertionError('account must not be consulted when ROCKETRIDE_DB_DSN is injected') + + monkeypatch.setitem( + sys.modules, 'ai.account', _types.SimpleNamespace(account=_types.SimpleNamespace(resolve_db_dsn=poisoned)) + ) + monkeypatch.setenv('ROCKETRIDE_DB_DSN', TEST_DSN) + glb, inst = self._begin(monkeypatch, {'table': sql_table, 'allow_execute': True}) + try: + assert glb.database == TEST_DB_NAME + rows = inst.execute({'sql': f'SELECT count(*) AS n FROM {sql_table}'}) + assert rows['rows'][0]['n'] == 2 + finally: + glb.endGlobal() + + def test_config_connection_fields_ignored(self, rr_env, monkeypatch, sql_table): + """The defining property: config cannot redirect the connection.""" + glb, _ = self._begin( + monkeypatch, + {'table': sql_table, 'host': 'evil.example.com', 'user': 'evil', 'password': 'evil'}, + ) + try: + assert glb.database == TEST_DB_NAME + assert 'evil' not in str(glb.engine.url) + finally: + glb.endGlobal() + + +# --------------------------------------------------------------------------- +# rocketride_vector end-to-end (incl. HNSW index verification) +# --------------------------------------------------------------------------- + + +def _load_vector_store(monkeypatch): + iglobal = _load_from_path('nodes.rocketride_vector.IGlobal', _VEC_NODE_DIR / 'IGlobal.py') + pkg = types.ModuleType('nodes.rocketride_vector') + pkg.__path__ = [str(_VEC_NODE_DIR)] + pkg.IGlobal = iglobal + monkeypatch.setitem(sys.modules, 'nodes.rocketride_vector', pkg) + monkeypatch.setitem(sys.modules, 'nodes.rocketride_vector.IGlobal', iglobal) + store = _load_from_path('nodes.rocketride_vector.rocketride_vector', _VEC_NODE_DIR / 'rocketride_vector.py') + return store.Store + + +VEC_TABLE = 'rr_vec_e2e' +DIMS = 8 + + +def _unit_vec(rng): + v = rng.standard_normal(DIMS) + return (v / np.linalg.norm(v)).tolist() + + +class TestRocketrideVectorE2E: + @pytest.fixture() + def vec_store(self, rr_env, monkeypatch, raw_conn): + with raw_conn.cursor() as cur: + cur.execute(f'DROP TABLE IF EXISTS {VEC_TABLE}') + raw_conn.commit() + store_cls = _load_vector_store(monkeypatch) + store = store_cls('rocketride_vector', {'collection': VEC_TABLE, 'similarity': 'cosine'}, {}) + yield store + # Close the driver connection before dropping: the driver's SELECT + # paths (mirroring vectordb_postgres) leave the session idle in + # transaction, and its AccessShare lock would block the DROP. + if store.client is not None: + store.client.close() + store.client = None + with raw_conn.cursor() as cur: + cur.execute(f'DROP TABLE IF EXISTS {VEC_TABLE}') + raw_conn.commit() + + def _chunks(self, schema, contents_and_embeddings): + docs = [] + for i, (content, embedding) in enumerate(contents_and_embeddings): + docs.append( + schema.Doc( + page_content=content, + metadata={ + 'objectId': f'obj-{i}', + 'nodeId': 'test-node', + 'parent': '/test', + 'permissionId': 0, + 'isDeleted': False, + 'chunkId': 0, + 'isTable': False, + 'tableId': 0, + 'vectorSize': len(embedding), + 'modelName': 'test-model', + }, + embedding=embedding, + embedding_model='test-model', + ) + ) + return docs + + def test_first_write_creates_table_and_hnsw_index(self, rr_env, vec_store, raw_conn): + rng = np.random.default_rng(7) + vec_store.addChunks(self._chunks(rr_env.schema, [(f'doc {i}', _unit_vec(rng)) for i in range(100)])) + + with raw_conn.cursor() as cur: + cur.execute('SELECT indexname, indexdef FROM pg_indexes WHERE tablename = %s', (VEC_TABLE,)) + indexes = {name: ddl for name, ddl in cur.fetchall()} + + index_name = f'{VEC_TABLE}_embedding_hnsw' + assert index_name in indexes + assert 'USING hnsw (embedding vector_cosine_ops)' in indexes[index_name] + assert "m='16'" in indexes[index_name] + assert "ef_construction='64'" in indexes[index_name] + + def test_semantic_search_uses_hnsw_index_not_seq_scan(self, rr_env, vec_store, raw_conn): + """The acceptance check: the node's own search SQL must use the index. + + The planner rightly prefers a seq scan on tiny tables, so load enough + rows (and ANALYZE) that index cost wins; then EXPLAIN the exact + ``semantic_search`` statement the driver executes. + """ + rng = np.random.default_rng(11) + # Insert in one bulk statement (the driver's row-at-a-time insert is + # too slow for a 2000-row planner fixture); create the table + index + # through the driver's first-write path first. + vec_store.addChunks(self._chunks(rr_env.schema, [('seed doc', _unit_vec(rng))])) + with raw_conn.cursor() as cur: + cur.execute( + f'INSERT INTO {VEC_TABLE} (content, objectId, chunkId, isDeleted, embedding) ' + f"SELECT 'doc '||i, 'obj-'||i, 0, false, " + f"(SELECT ('['||string_agg((random()*2-1)::text, ',')||']')::vector " + f' FROM generate_series(1,{DIMS}) WHERE i=i) ' + f'FROM generate_series(1, 2000) i' + ) + cur.execute(f'ANALYZE {VEC_TABLE}') + raw_conn.commit() + + # EXPLAIN the exact statement the driver runs for semantic search. + node_module = sys.modules['nodes.rocketride_vector.rocketride_vector'] + search_sql = node_module.SQL_QUERIES['semantic_search'].format( + collection=VEC_TABLE, similarity_operator='<=>', where_clause='' + ) + probe = _unit_vec(rng) + with raw_conn.cursor() as cur: + cur.execute(f'EXPLAIN {search_sql}', (str(probe), 5)) + plan = '\n'.join(row[0] for row in cur.fetchall()) + + assert f'Index Scan using {VEC_TABLE}_embedding_hnsw' in plan, plan + assert 'Seq Scan' not in plan, plan + + def test_semantic_search_returns_nearest_document(self, rr_env, vec_store): + rng = np.random.default_rng(23) + vectors = [(f'doc {i}', _unit_vec(rng)) for i in range(50)] + vec_store.addChunks(self._chunks(rr_env.schema, vectors)) + + # Query with (nearly) the exact embedding of doc 17 — it must come back first. + target = vectors[17][1] + query = rr_env.schema.QuestionText('find doc 17') + query.embedding = target + results = vec_store.searchSemantic(query, rr_env.schema.DocFilter(limit=5)) + assert results, 'semantic search returned nothing' + assert results[0].page_content == 'doc 17' + assert results[0].score > 0.99 + + def test_upsert_replaces_same_object_id(self, rr_env, vec_store): + rng = np.random.default_rng(31) + first = self._chunks(rr_env.schema, [('original', _unit_vec(rng))]) + vec_store.addChunks(first) + replacement = self._chunks(rr_env.schema, [('replaced', _unit_vec(rng))]) + vec_store.addChunks(replacement) + assert vec_store.count_documents() == 1 + + def test_wide_vectors_skip_index_with_warning(self, rr_env, monkeypatch, raw_conn): + wide_table = 'rr_vec_wide_e2e' + with raw_conn.cursor() as cur: + cur.execute(f'DROP TABLE IF EXISTS {wide_table}') + raw_conn.commit() + store_cls = _load_vector_store(monkeypatch) + store = store_cls('rocketride_vector', {'collection': wide_table, 'similarity': 'cosine'}, {}) + try: + store._createCollection(vectorSize=2001) + with raw_conn.cursor() as cur: + cur.execute('SELECT indexname FROM pg_indexes WHERE tablename = %s', (wide_table,)) + index_names = [row[0] for row in cur.fetchall()] + assert not any('hnsw' in name for name in index_names) + assert any('Skipping index' in w for w in rr_env.warnings) + finally: + with raw_conn.cursor() as cur: + cur.execute(f'DROP TABLE IF EXISTS {wide_table}') + raw_conn.commit() diff --git a/nodes/test/test_rocketride_graph_full.py b/nodes/test/test_rocketride_graph_full.py new file mode 100644 index 000000000..337d1e76c --- /dev/null +++ b/nodes/test/test_rocketride_graph_full.py @@ -0,0 +1,401 @@ +# ============================================================================= +# RocketRide Engine +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Integration tests for rocketride_graph against a real Postgres + Apache AGE. + +Runs the node end-to-end on the live pin (PG16 + AGE 1.5.0) through the real +seam (env ``ROCKETRIDE_CLIENT_ID`` + a fake, injectable +``Account.resolve_db_dsn``). Container: + + docker run -d --name rr-pg-phase2 -p 55433:5432 \ + -e POSTGRES_PASSWORD=rrpass -e POSTGRES_USER=rruser -e POSTGRES_DB=rrtenant \ + rr-age-pin:pg16-age1.5.0-pgvector0.8.0 + # image: apache/age:release_PG16_1.5.0 + pgvector v0.8.0 (see the layer README) + +Configuration via environment variables: + RR_TEST_AGE_DSN — libpq URL of the AGE test database + (default: postgresql://rruser:rrpass@localhost:55433/rrtenant) + +Skips cleanly when the database is unreachable. +""" + +from __future__ import annotations + +import importlib.util +import os +import sys +import types + +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parents[2] +_AI_SRC = _REPO / 'packages' / 'ai' / 'src' +_GRAPH_PKG = _AI_SRC / 'ai' / 'common' / 'graph' +_UTILS_DIR = _AI_SRC / 'ai' / 'common' / 'utils' +_RRDB_PATH = _AI_SRC / 'ai' / 'common' / 'rocketride_db.py' +_NODE_DIR = _REPO / 'nodes' / 'src' / 'nodes' / 'rocketride_graph' + +TEST_DSN = os.environ.get('RR_TEST_AGE_DSN', 'postgresql://rruser:rrpass@localhost:55433/rrtenant') +TEST_CLIENT_ID = 'tenant-graph-test' +GRAPH = 'rr_graph_e2e' + +# RR_REQUIRE_DB_TESTS (set by CI once its DB containers are healthy) turns +# every skip in this module into a hard failure: a broken container or missing +# dependency must never let the safety-control tests silently go green. +_DB_TESTS_REQUIRED = bool(os.environ.get('RR_REQUIRE_DB_TESTS')) + +if _DB_TESTS_REQUIRED: + import antlr4 # noqa: F401 + import psycopg2 +else: + psycopg2 = pytest.importorskip('psycopg2') + pytest.importorskip('antlr4') + + +def _db_reachable() -> bool: + try: + conn = psycopg2.connect(TEST_DSN, connect_timeout=3) + conn.close() + return True + except Exception: + return False + + +if _DB_TESTS_REQUIRED and not _db_reachable(): + pytest.fail(f'RR_REQUIRE_DB_TESTS is set but the AGE test database is not reachable at {TEST_DSN}', pytrace=False) + +pytestmark = pytest.mark.skipif(not _db_reachable(), reason=f'RocketRide AGE test database not reachable at {TEST_DSN}') + + +def _load_from_path(name: str, path: Path, *, is_package: bool = False): + search = [str(path.parent)] if is_package else None + spec = importlib.util.spec_from_file_location(name, path, submodule_search_locations=search) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +@pytest.fixture() +def rr_env(monkeypatch): + """Stub the engine surface, load real graph base + age layer + node.""" + warnings: list[str] = [] + + depends_mod = types.ModuleType('depends') + depends_mod.depends = lambda *a, **kw: None + monkeypatch.setitem(sys.modules, 'depends', depends_mod) + + rocketlib = types.ModuleType('rocketlib') + rocketlib.IGlobalBase = type('IGlobalBase', (), {}) + rocketlib.IInstanceBase = type('IInstanceBase', (), {}) + rocketlib.tool_function = lambda **kwargs: lambda f: f + rocketlib.debug = lambda *a, **kw: None + rocketlib.error = lambda *a, **kw: None + rocketlib.warning = warnings.append + rocketlib.OPEN_MODE = types.SimpleNamespace(CONFIG=object()) + rocketlib_types = types.ModuleType('rocketlib.types') + rocketlib_types.IInvokeLLM = types.SimpleNamespace(Ask=lambda **kw: kw) + rocketlib.types = rocketlib_types + monkeypatch.setitem(sys.modules, 'rocketlib', rocketlib) + monkeypatch.setitem(sys.modules, 'rocketlib.types', rocketlib_types) + + ai_pkg = types.ModuleType('ai') + ai_pkg.__path__ = [] + common_pkg = types.ModuleType('ai.common') + common_pkg.__path__ = [str(_AI_SRC / 'ai' / 'common')] + + schema = types.ModuleType('ai.common.schema') + schema.Answer = type('Answer', (), {'setAnswer': lambda self, v: setattr(self, 'value', v)}) + schema.Question = type('Question', (), {}) + schema.QuestionType = types.SimpleNamespace(QUESTION=1, DIALECT=2, EXECUTE=3) + + table = types.ModuleType('ai.common.table') + table.Table = types.SimpleNamespace( + generate_markdown_table=lambda data, headers=None: '\n'.join(' | '.join(map(str, r)) for r in data) + ) + + config = types.ModuleType('ai.common.config') + config.Config = types.SimpleNamespace(getNodeConfig=lambda provider, connConfig: dict(connConfig)) + + monkeypatch.setitem(sys.modules, 'ai', ai_pkg) + monkeypatch.setitem(sys.modules, 'ai.common', common_pkg) + monkeypatch.setitem(sys.modules, 'ai.common.schema', schema) + monkeypatch.setitem(sys.modules, 'ai.common.table', table) + monkeypatch.setitem(sys.modules, 'ai.common.config', config) + + config_utils = _load_from_path('ai.common.config_utils', _UTILS_DIR / 'config_utils.py') + tool_args = _load_from_path('ai.common.tool_args', _UTILS_DIR / 'tool_args.py') + utils = types.ModuleType('ai.common.utils') + utils.parse_bool = config_utils.parse_bool + utils.config_int = config_utils.config_int + for fn in ('normalize_tool_input', 'require_str', 'require_dict', 'require_int', 'optional_int', 'optional_str'): + setattr(utils, fn, getattr(tool_args, fn)) + monkeypatch.setitem(sys.modules, 'ai.common.utils', utils) + + rrdb_mod = _load_from_path('ai.common.rocketride_db', _RRDB_PATH) + monkeypatch.setitem(sys.modules, 'ai.common.rocketride_db', rrdb_mod) + + # Real graph base package (brings ai.common.graph.age with it). + graph_pkg = _load_from_path('ai.common.graph', _GRAPH_PKG / '__init__.py', is_package=True) + monkeypatch.setitem(sys.modules, 'ai.common.graph', graph_pkg) + + # The injectable fake resolver — the same seam a SaaS build fills in. + account_mod = types.ModuleType('ai.account') + + async def fake_resolve_db_dsn(client_id): + assert client_id == TEST_CLIENT_ID + return TEST_DSN + + account_mod.account = types.SimpleNamespace(resolve_db_dsn=fake_resolve_db_dsn) + monkeypatch.setitem(sys.modules, 'ai.account', account_mod) + monkeypatch.setenv('ROCKETRIDE_CLIENT_ID', TEST_CLIENT_ID) + monkeypatch.delenv('ROCKETRIDE_DB_DSN', raising=False) + + iglobal = _load_from_path('nodes.rocketride_graph.IGlobal', _NODE_DIR / 'IGlobal.py') + pkg = types.ModuleType('nodes.rocketride_graph') + pkg.__path__ = [str(_NODE_DIR)] + pkg.IGlobal = iglobal + monkeypatch.setitem(sys.modules, 'nodes.rocketride_graph', pkg) + monkeypatch.setitem(sys.modules, 'nodes.rocketride_graph.IGlobal', iglobal) + iinstance = _load_from_path('nodes.rocketride_graph.IInstance', _NODE_DIR / 'IInstance.py') + + return types.SimpleNamespace(iglobal_cls=iglobal.IGlobal, iinstance_cls=iinstance.IInstance, warnings=warnings) + + +@pytest.fixture() +def age_graph(rr_env): + """Provision + seed the test graph out-of-band (provisioning is not the + node's job while create_graph ownership is open). + """ + conn = psycopg2.connect(TEST_DSN) + conn.autocommit = True + with conn.cursor() as cur: + cur.execute('SET search_path = ag_catalog, "$user", public') + cur.execute('SELECT 1 FROM ag_graph WHERE name = %s', (GRAPH,)) + if cur.fetchone(): + cur.execute('SELECT drop_graph(%s, true)', (GRAPH,)) + cur.execute('SELECT create_graph(%s)', (GRAPH,)) + cur.execute( + f"SELECT * FROM cypher('{GRAPH}', $$ " + "CREATE (a:Person {name:'alice', age: 30})-[:KNOWS {since: 2019}]->(b:Person {name:'bob', age: 25}), " + "(b)-[:KNOWS {since: 2021}]->(c:Person {name:'carol', age: 41}), " + "(a)-[:WORKS_AT]->(x:Company {name:'RocketRide'}) " + '$$) AS (v agtype)' + ) + yield conn + with conn.cursor() as cur: + cur.execute('SET search_path = ag_catalog, "$user", public') + cur.execute('SELECT drop_graph(%s, true)', (GRAPH,)) + conn.close() + + +def _begin(rr_env, config=None): + glb = rr_env.iglobal_cls() + merged = {'graph': GRAPH, **(config or {})} + glb.glb = types.SimpleNamespace(logicalType='rocketride_graph', connConfig=merged) + glb.beginGlobal() + return glb + + +class TestLifecycle: + def test_begin_global_connects_and_reflects(self, rr_env, age_graph): + glb = _begin(rr_env) + try: + assert glb.age_version == '1.5.0' + assert glb.graph_name == GRAPH + assert set(glb.graph_schema['nodes']) == {'Person', 'Company'} + props = dict(glb.graph_schema['nodes']['Person']) + assert props.get('name') == 'str' + assert props.get('age') == 'int' + rel_types = {r['type'] for r in glb.graph_schema['relationships']} + assert rel_types == {'KNOWS', 'WORKS_AT'} + knows = next(r for r in glb.graph_schema['relationships'] if r['type'] == 'KNOWS') + assert knows['start'] == 'Person' and knows['end'] == 'Person' + finally: + glb.endGlobal() + + def test_missing_graph_fails_fast(self, rr_env, age_graph): + glb = rr_env.iglobal_cls() + glb.glb = types.SimpleNamespace(logicalType='rocketride_graph', connConfig={'graph': 'rr_graph_missing'}) + with pytest.raises(RuntimeError, match='does not exist'): + glb.beginGlobal() + + +class TestSafePath: + def test_read_query_decodes_rows(self, rr_env, age_graph): + glb = _begin(rr_env) + try: + rows = glb._run_query( + 'MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a.name AS who, r.since AS since, b ORDER BY r.since' + ) + assert [r['who'] for r in rows] == ['alice', 'bob'] + assert rows[0]['since'] == 2019 + assert rows[0]['b']['label'] == 'Person' + assert rows[0]['b']['properties']['name'] == 'bob' + finally: + glb.endGlobal() + + def test_params_round_trip(self, rr_env, age_graph): + glb = _begin(rr_env) + try: + rows = glb._run_query('MATCH (p:Person) WHERE p.name = $who RETURN p.age AS age', params={'who': 'carol'}) + assert rows == [{'age': 41}] + finally: + glb.endGlobal() + + def test_limit_contract_returns_at_most_limit_plus_one(self, rr_env, age_graph): + glb = _begin(rr_env) + try: + rows = glb._run_query('MATCH (p:Person) RETURN p.name AS name', limit=1) + assert len(rows) == 2 # limit + 1 => the base detects truncation + finally: + glb.endGlobal() + + def test_read_only_transaction_enforced_at_database(self, rr_env, age_graph): + """Read back SHOW transaction_read_only inside a safe plan: it must be + 'on'. Outside a transaction both SET TRANSACTION READ ONLY and SET + LOCAL are silent no-ops, so this pins the precondition (autocommit + off -> implicit transaction) that the write protection and the + statement_timeout cap both hang on. + """ + age = sys.modules['ai.common.graph.age'] + glb = _begin(rr_env) + try: + assert glb.client.autocommit is False + plan = age.TranslatedQuery(columns=['ro'], has_return=True) + plan.statements.append('SHOW transaction_read_only') + plan.binds.append(()) + plan.result_index = 0 + plan.read_only = True + rows = glb._execute_plan(plan, fetch_cap=1) + assert rows and rows[0][0] == 'on' + finally: + glb.endGlobal() + + def test_write_rejected_before_reaching_db(self, rr_env, age_graph): + glb = _begin(rr_env) + try: + with pytest.raises(Exception, match='write_clause'): + glb._run_query("CREATE (n:Person {name: 'evil'})") + finally: + glb.endGlobal() + + def test_unsupported_datetime_rejected(self, rr_env, age_graph): + glb = _begin(rr_env) + try: + with pytest.raises(Exception, match='datetime'): + glb._run_query('MATCH (n) RETURN datetime()') + finally: + glb.endGlobal() + + +class TestExecutePath: + def test_execute_gated_by_allow_execute(self, rr_env, age_graph): + glb = _begin(rr_env, {'allow_execute': False}) + inst = rr_env.iinstance_cls() + inst.IGlobal = glb + try: + with pytest.raises(ValueError, match='execute tool is disabled'): + inst.execute({'query': 'MATCH (n) RETURN n.name'}) + finally: + glb.endGlobal() + + def test_execute_write_and_read_back(self, rr_env, age_graph): + glb = _begin(rr_env, {'allow_execute': True}) + inst = rr_env.iinstance_cls() + inst.IGlobal = glb + try: + written = inst.execute({'query': "CREATE (d:Person {name: 'dave', age: 8}) RETURN d.name AS name"}) + assert written['rows'] == [{'name': 'dave'}] + back = glb._run_query('MATCH (p:Person) WHERE p.name = $n RETURN p.age AS age', params={'n': 'dave'}) + assert back == [{'age': 8}] + finally: + glb.endGlobal() + + def test_execute_still_enforces_resource_caps(self, rr_env, age_graph): + glb = _begin(rr_env, {'allow_execute': True}) + inst = rr_env.iinstance_cls() + inst.IGlobal = glb + try: + with pytest.raises(Exception, match='unbounded_var_length'): + inst.execute({'query': 'MATCH (a)-[*]->(b) RETURN a'}) + finally: + glb.endGlobal() + + +class TestValidateQuery: + def test_valid_query_passes(self, rr_env, age_graph): + glb = _begin(rr_env) + try: + ok, err = glb._validate_query('MATCH (p:Person) RETURN p.name') + assert ok, err + finally: + glb.endGlobal() + + def test_syntax_error_reported_for_repair_loop(self, rr_env, age_graph): + glb = _begin(rr_env) + try: + ok, err = glb._validate_query('MATCHX (p RETURN p') + assert not ok + assert 'syntax error' in err + finally: + glb.endGlobal() + + def test_firewall_rejection_reported(self, rr_env, age_graph): + glb = _begin(rr_env) + try: + ok, err = glb._validate_query('MATCH (a)-[*]->(b) RETURN a') + assert not ok + assert 'unbounded_var_length' in err + finally: + glb.endGlobal() + + def test_validate_does_not_execute(self, rr_env, age_graph): + glb = _begin(rr_env) + try: + before = glb._run_query('MATCH (p:Person) RETURN count(p) AS n')[0]['n'] + # EXPLAIN of a read; graph must be unchanged afterwards. + ok, _ = glb._validate_query('MATCH (p:Person) RETURN p.name') + assert ok + after = glb._run_query('MATCH (p:Person) RETURN count(p) AS n')[0]['n'] + assert before == after + finally: + glb.endGlobal() + + +class TestInstanceTools: + def test_dialect_and_schema_tools(self, rr_env, age_graph): + glb = _begin(rr_env) + inst = rr_env.iinstance_cls() + inst.IGlobal = glb + try: + assert inst.dialect({}) == {'dialect': 'age'} + schema = inst.get_schema({}) + assert 'Person' in schema['labels'] + finally: + glb.endGlobal() diff --git a/packages/ai/src/ai/account/base.py b/packages/ai/src/ai/account/base.py index 7ccbe0c30..75190a0ed 100644 --- a/packages/ai/src/ai/account/base.py +++ b/packages/ai/src/ai/account/base.py @@ -318,6 +318,139 @@ async def get_transactions( """ return {'transactions': [], 'total': 0, 'page': page, 'pageSize': page_size} + # ========================================================================= + # CLOUD DATABASE — env-gated broker call; raises when unconfigured + # ========================================================================= + + async def resolve_db_dsn(self, client_id: str) -> str: + """ + Resolve the per-tenant database DSN for the RocketRide cloud DB nodes. + + The ``rocketride_sql`` / ``rocketride_vector`` / ``rocketride_graph`` + nodes take no connection configuration. Instead of reading + host/user/password, they resolve a ready connection string for the + caller's own provisioned cloud database, keyed by the authenticated + ``client_id`` (``userId``). One database per tenant backs all three. + + Default implementation (both editions): call the data-core provisioner + configured via server-side environment — + + ROCKETRIDE_DB_BROKER_URL the /provision endpoint URL + ROCKETRIDE_DB_BROKER_TOKEN its Bearer token (from ASM/k8s; never + hardcoded) + + ``POST {url} {"tenant_id": client_id}`` -> ``{"database", "role", + "dsn", "created"}`` (only ``dsn`` is read here). + The endpoint is idempotent (same tenant -> same DSN; the per-tenant + password is derived, not stored), so this method is safe to call on + every task start with no caching or persistence on this side. + + When the environment is not configured (the open-source default), this + raises: the cloud databases require a RocketRide cloud deployment. + A SaaS overlay may still override this method entirely. + + Args: + client_id: The authenticated connection identity (``userId``). + Passed verbatim as the broker's ``tenant_id`` (the provisioner + owns slugging/hashing it into a database name). + + Returns: + A libpq/SQLAlchemy-compatible PostgreSQL DSN for the tenant DB + (e.g. ``postgresql://role:pass@pooler:5432/t_?sslmode=require``). + + Raises: + NotImplementedError: broker environment not configured. + RuntimeError: the broker rejected the request or returned no DSN. + """ + broker_url = os.environ.get('ROCKETRIDE_DB_BROKER_URL', '').strip() + broker_token = os.environ.get('ROCKETRIDE_DB_BROKER_TOKEN', '').strip() + if not broker_url or not broker_token: + raise NotImplementedError('RocketRide cloud DB nodes require signing into RocketRide cloud') + if not client_id or not client_id.strip(): + raise ValueError('resolve_db_dsn requires a non-empty client_id') + self._check_broker_url(broker_url) + + import asyncio + + return await asyncio.to_thread(self._call_db_broker, broker_url, broker_token, client_id.strip()) + + @staticmethod + def _check_broker_url(url: str) -> None: + """Refuse to send the broker token over anything but HTTPS. + + The token can resolve ANY tenant's DSN, so a deployment typo like an + ``http://`` broker URL must fail loudly, not silently ship the token + (and every returned DSN) in cleartext. Plain http is allowed only for + localhost — the local dev/test rig runs the provisioner there. + """ + from urllib.parse import urlparse + + try: + parsed = urlparse(url) + scheme, host = parsed.scheme.lower(), parsed.hostname + except ValueError: + scheme, host = '', None + if scheme == 'https': + return + if scheme == 'http' and host in ('localhost', '127.0.0.1', '::1'): + return + raise RuntimeError( + f'ROCKETRIDE_DB_BROKER_URL must use https (got {scheme or "no"} scheme): ' + 'the broker token must never travel unencrypted; plain http is allowed only for localhost' + ) + + @staticmethod + def _call_db_broker(url: str, token: str, tenant_id: str) -> str: + """Blocking POST to the provisioner; runs in a worker thread. + + Stdlib-only on purpose: the account layer has no async-HTTP dependency, + and one small request per task start does not justify adding one. + """ + import json + import urllib.error + import urllib.request + + request = urllib.request.Request( + url, + data=json.dumps({'tenant_id': tenant_id}).encode('utf-8'), + headers={ + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + }, + method='POST', + ) + try: + with urllib.request.urlopen(request, timeout=15) as response: + body = json.loads(response.read().decode('utf-8')) + except urllib.error.HTTPError as e: + # 4xx = unknown/invalid tenant per the broker contract; 5xx = broker fault. + raise RuntimeError(f'DB broker rejected provision for this account (HTTP {e.code})') from e + except Exception as e: + raise RuntimeError(f'DB broker unreachable: {e}') from e + + dsn = body.get('dsn') if isinstance(body, dict) else None + if not dsn or not isinstance(dsn, str): + raise RuntimeError('DB broker response did not include a DSN') + return AccountBase._pin_dsn_tls(dsn) + + @staticmethod + def _pin_dsn_tls(dsn: str) -> str: + """Validate the broker's DSN and guarantee it carries an sslmode. + + The broker contract sends ``sslmode=require``; this pins that + guarantee client-side so a broker regression cannot silently + downgrade every tenant connection to cleartext. + """ + from urllib.parse import parse_qs, urlparse + + parsed = urlparse(dsn) + if parsed.scheme.lower() not in ('postgres', 'postgresql'): + raise RuntimeError(f'DB broker returned a non-PostgreSQL DSN (scheme {parsed.scheme!r})') + if 'sslmode' in parse_qs(parsed.query): + return dsn + separator = '&' if parsed.query else '?' + return f'{dsn}{separator}sslmode=require' + # ========================================================================= # DAP COMMAND DISPATCH — SaaS overrides all three # ========================================================================= diff --git a/packages/ai/src/ai/account/oss/__init__.py b/packages/ai/src/ai/account/oss/__init__.py index 8f0bad986..86424f7fd 100644 --- a/packages/ai/src/ai/account/oss/__init__.py +++ b/packages/ai/src/ai/account/oss/__init__.py @@ -233,6 +233,10 @@ async def remove_team_member(self, **kw): # audit() is inherited from AccountBase as a no-op — OSS has no database. + # resolve_db_dsn is inherited from AccountBase: env-gated broker call + # (ROCKETRIDE_DB_BROKER_URL/_TOKEN); raises the cloud-sign-in error when + # the environment is not configured — the open-source default. + # ========================================================================= # APP MANIFEST — read from static apps.json # ========================================================================= diff --git a/packages/ai/src/ai/common/graph/age/README.md b/packages/ai/src/ai/common/graph/age/README.md new file mode 100644 index 000000000..2edf5a2a8 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/README.md @@ -0,0 +1,100 @@ +# Cypher → Apache AGE translation layer + +Apache AGE cannot run bare Cypher: every query needs an SQL `cypher()` +envelope, a synthesized result-column list, prepared-statement parameter +binding, and agtype result decoding — plus a firewall and a dialect gate for +the version gaps. This package is that translation, as a **pure transform** +(no database access), so graph nodes targeting the RocketRide cloud data-core +(PG16 + AGE 1.5.0) can speak Cypher end to end. + +## Pipeline + +`translate(cypher, params, limit, mode, graph_name, age_version, firewall)` +→ [`TranslatedQuery`](emit.py): + +1. **Parse** ([analysis.py](analysis.py)) — openCypher ANTLR grammar (M23), + not regex. Extracts RETURN projection, write clauses, `$params`, + variable-length depth bounds, invoked functions. +2. **Firewall** ([firewall.py](firewall.py)) — resource caps on **both** + paths (query length, variable-length depth, statement timeout); semantic + read-only rules (no writes, no CALL) on the **safe** path only. +3. **Dialect** ([capabilities.py](capabilities.py)) — capability table keyed + by AGE version: `SUPPORTED` / `EMULATE` (rewrite hook; framework only in + v1) / `REJECT` (fail loud pre-flight) / `TBD` (unverified: passes through, + AGE's own error surfaces via EXPLAIN/execute). Verify TBD cells against + the live instance and promote them. +4. **Emit** ([emit.py](emit.py)) — `cypher()` envelope with a + collision-proof dollar-quote tag, synthesized `AS (c0 agtype, …)` list, + `SET LOCAL search_path`/`statement_timeout` preamble, and — when params + are present — `PREPARE …(agtype)` / `EXECUTE …(%s::agtype)` / + `DEALLOCATE` (AGE rejects inline `::agtype` literals as the params + argument). +5. **Decode** ([decode.py](decode.py)) — agtype text → plain Python via the + vendored Apache AGE driver parser; vertices/edges/paths flatten to dicts. + +The caller (the `rocketride_graph` node) executes `TranslatedQuery.statements` +in order inside one transaction — `BEGIN READ ONLY` on the safe path — and +decodes rows with `decode_row`. Everything is `SET LOCAL` because the cloud +endpoint is a transaction-mode pooler: plain `SET` would bleed across tenants. + +Errors: `AgeTranslationError` (parse/emit), `AgeUnsupportedFeature` +(capability REJECT), `AgeFirewallRejected` (caps/semantics) — all fail loud. + +## Verified AGE 1.5.0 mechanics + +Probed against a container on the live pin (PG 16.14 + AGE 1.5.0 + pgvector +0.8.0, image `apache/age:release_PG16_1.5.0` + pgvector v0.8.0): + +- AS-list column **count** must equal the RETURN count (names arbitrary); + no-RETURN statements accept one synthesized column and yield 0 rows. +- `RETURN *` requires scope analysis to synthesize columns → REJECT in v1. +- cypher()'s third argument must be a **prepared-statement parameter** + (inline `'…'::agtype` fails: "third argument of cypher function must be a + parameter"). +- `age` is in `shared_preload_libraries` → no `LOAD 'age'` needed. +- `EXPLAIN` works over the envelope (incl. `EXPLAIN EXECUTE`) and surfaces + Cypher syntax errors cleanly — that is the `_validate_query` mechanism. +- `BEGIN READ ONLY` blocks writes through `cypher()` server-side ("cannot + execute CREATE TABLE in a read-only transaction"). +- `datetime()` does not exist on 1.5.0 (`ag_catalog.age_datetime` missing) + → capability REJECT. + +## Vendored code + +| Path | Origin | License | +|---|---|---| +| `_cypher/Cypher.g4` + `_cypher/gen/` | openCypher **M23** grammar artifact (`https://s3.amazonaws.com/artifacts.opencypher.org/M23/Cypher.g4`), parser generated with ANTLR 4.13.2 | Apache-2.0 (© Neo Technology 2015-2023) | +| `_agtype/` (`builder.py`, `models.py`, `exceptions.py`, `Agtype.g4`) | `apache/age` master `drivers/python/age/` + `drivers/Agtype.g4` at commit `5a254d6869d8b2c271f025ea158c0fee2cfacfa3` (PyPI `apache-age-python` 0.0.7 is stale and hard-pins antlr 4.11.1 — deliberately not a dependency) | Apache-2.0 | +| `_agtype/gen/` | regenerated from `Agtype.g4` with ANTLR 4.13.2 (upstream ships 4.11-generated code that warns per-parse under the 4.13 runtime) | Apache-2.0 | + +Local deviations are documented in each `_agtype`/`_cypher` `__init__.py` +(notably: dropped `_agtype/exceptions.py`'s `from psycopg.errors import *` +psycopg-v3 re-export). + +Runtime dependency: `antlr4-python3-runtime>=4.13.2,<4.14` +(declared in `packages/ai/src/ai/common/requirements.txt`). + +## Regenerating the parsers + +Only needed when a grammar file changes. Requires the pip package +`antlr4-tools` (auto-downloads the ANTLR jar; needs a JRE — `install-jdk` +can fetch one without touching the system): + +```bash +antlr4 -v 4.13.2 -Dlanguage=Python3 -visitor -o _cypher/gen _cypher/Cypher.g4 +antlr4 -v 4.13.2 -Dlanguage=Python3 -visitor -o _agtype/gen _agtype/Agtype.g4 +# then: delete the *.interp/*.tokens artifacts, restore gen/__init__.py, +# and normalize the "Generated from " headers to bare filenames. +``` + +## Deliberately out of scope (v1) + +- ~~Capability cell values for `merge_on_set`, `where_label_check`, + `multi_label`, `shortest_path`~~ — **verified 2026-07-28** against the exact + pin container (PG 16.14 + AGE 1.5.0): all four fail at AGE's parser + (`syntax error at or near ...`) while plain `MERGE` on the same graph + succeeds, so all four are promoted to `REJECT` with actionable messages. + No `TBD` cells remain in the 1.5.0 table. +- `EMULATE` rewrites (framework hook exists, no emulations implemented). +- Capability routing to FalkorDB/Neo4j for AGE-can't-do workloads (own + effort, per the design). diff --git a/packages/ai/src/ai/common/graph/age/__init__.py b/packages/ai/src/ai/common/graph/age/__init__.py new file mode 100644 index 000000000..dd3f655c2 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/__init__.py @@ -0,0 +1,56 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Cypher -> Apache AGE translation layer. + +Public surface for graph nodes targeting the RocketRide cloud data-core +(Postgres + Apache AGE). See README.md in this directory for the pipeline +description, verified AGE mechanics, and vendored-code provenance. +""" + +from .analysis import CypherFacts, analyze +from .capabilities import AGE_1_5_0, CAPABILITY_TABLES, Capability, CellStatus, DEFAULT_AGE_VERSION +from .decode import decode_agtype +from .emit import TranslatedQuery +from .errors import AgeFirewallRejected, AgeTranslationError, AgeUnsupportedFeature +from .firewall import FirewallConfig +from .translate import TranslateMode, decode_row, translate + +__all__ = [ + 'AGE_1_5_0', + 'AgeFirewallRejected', + 'AgeTranslationError', + 'AgeUnsupportedFeature', + 'CAPABILITY_TABLES', + 'Capability', + 'CellStatus', + 'CypherFacts', + 'DEFAULT_AGE_VERSION', + 'FirewallConfig', + 'TranslateMode', + 'TranslatedQuery', + 'analyze', + 'decode_agtype', + 'decode_row', + 'translate', +] diff --git a/packages/ai/src/ai/common/graph/age/_agtype/Agtype.g4 b/packages/ai/src/ai/common/graph/age/_agtype/Agtype.g4 new file mode 100644 index 000000000..1d75c1c3d --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_agtype/Agtype.g4 @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +grammar Agtype; + +agType + : agValue EOF + ; + +agValue + : value typeAnnotation? + ; + +value + : STRING #StringValue + | INTEGER #IntegerValue + | floatLiteral #FloatValue + | 'true' #TrueBoolean + | 'false' #FalseBoolean + | 'null' #NullValue + | obj #ObjectValue + | array #ArrayValue + ; + +obj + : '{' pair (',' pair)* '}' + | '{' '}' + ; + +pair + : STRING ':' agValue + ; + +array + : '[' agValue (',' agValue)* ']' + | '[' ']' + ; + +typeAnnotation + : '::' IDENT + ; + +IDENT + : [A-Z_a-z][$0-9A-Z_a-z]* + ; + +STRING + : '"' (ESC | SAFECODEPOINT)* '"' + ; + +fragment ESC + : '\\' (["\\/bfnrt] | UNICODE) + ; + +fragment UNICODE + : 'u' HEX HEX HEX HEX + ; + +fragment HEX + : [0-9a-fA-F] + ; + +fragment SAFECODEPOINT + : ~ ["\\\u0000-\u001F] + ; + +INTEGER + : '-'? INT + ; + +fragment INT + : '0' | [1-9] [0-9]* + ; + +floatLiteral + : RegularFloat + | ExponentFloat + | '-'? 'Infinity' + | 'NaN' + ; + +RegularFloat + : '-'? INT DECIMAL + ; + +ExponentFloat + : '-'? INT DECIMAL? SCIENTIFIC + ; + +fragment DECIMAL + : '.' [0-9]+ + ; + +fragment SCIENTIFIC + : [Ee][+-]? [0-9]+ + ; + +WS + : [ \t\n\r] + -> skip + ; diff --git a/packages/ai/src/ai/common/graph/age/_agtype/__init__.py b/packages/ai/src/ai/common/graph/age/_agtype/__init__.py new file mode 100644 index 000000000..84e0061df --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_agtype/__init__.py @@ -0,0 +1,19 @@ +# ============================================================================= +# VENDORED — Apache AGE Python driver agtype decoder (Apache-2.0). +# Source: apache/age master drivers/python/age/ at commit +# 5a254d6869d8b2c271f025ea158c0fee2cfacfa3 +# Local deviations: +# - exceptions.py: dropped 'from psycopg.errors import *' (psycopg v3 +# convenience re-export; this repo standardises on psycopg2 and the local +# exception classes never used it). +# - gen/: regenerated from drivers/Agtype.g4 with ANTLR 4.13.2 (upstream +# ships 4.11.1-generated code, which warns per-parse under the 4.13 +# runtime). See ../README.md for the regeneration step. +# - builder.py: _stripStringDelimiters decodes JSON escapes with json.loads +# (upstream strips only the quote delimiters, so '"a\nb"' round-tripped as +# a literal backslash-n; the STRING token is grammar-guaranteed to be a +# valid JSON string literal). +# ============================================================================= + +from .builder import newResultHandler, parseAgeValue # noqa: F401 +from .models import Edge, Path, Vertex # noqa: F401 diff --git a/packages/ai/src/ai/common/graph/age/_agtype/builder.py b/packages/ai/src/ai/common/graph/age/_agtype/builder.py new file mode 100644 index 000000000..b223634e9 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_agtype/builder.py @@ -0,0 +1,254 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +from . import gen +from .gen.AgtypeLexer import AgtypeLexer +from .gen.AgtypeParser import AgtypeParser +from .gen.AgtypeVisitor import AgtypeVisitor +from .models import * +from .exceptions import * +from antlr4 import InputStream, CommonTokenStream, ParserRuleContext +from antlr4.tree.Tree import TerminalNode +from decimal import Decimal +import json + +resultHandler = None + +class ResultHandler: + def parse(ageData): + pass + +def newResultHandler(query=""): + resultHandler = Antlr4ResultHandler(None, query) + return resultHandler + +def parseAgeValue(value, cursor=None): + if value is None: + return None + + global resultHandler + if (resultHandler == None): + resultHandler = Antlr4ResultHandler(None) + try: + return resultHandler.parse(value) + except Exception as ex: + raise AGTypeError(value, ex) + + +class Antlr4ResultHandler(ResultHandler): + def __init__(self, vertexCache, query=None): + self.lexer = AgtypeLexer() + self.parser = AgtypeParser(None) + self.visitor = ResultVisitor(vertexCache) + + def parse(self, ageData): + if not ageData: + return None + # print("Parse::", ageData) + + self.lexer.inputStream = InputStream(ageData) + self.parser.setTokenStream(CommonTokenStream(self.lexer)) + self.parser.reset() + tree = self.parser.agType() + parsed = tree.accept(self.visitor) + return parsed + + +# print raw result String +class DummyResultHandler(ResultHandler): + def parse(self, ageData): + print(ageData) + +# default agType visitor +class ResultVisitor(AgtypeVisitor): + vertexCache = None + + def __init__(self, cache) -> None: + super().__init__() + self.vertexCache = cache + + + def visitAgType(self, ctx:AgtypeParser.AgTypeContext): + agVal = ctx.agValue() + if agVal != None: + obj = ctx.agValue().accept(self) + return obj + + return None + + def visitAgValue(self, ctx:AgtypeParser.AgValueContext): + annoCtx = ctx.typeAnnotation() + valueCtx = ctx.value() + + if annoCtx is not None: + annoCtx.accept(self) + identNode = annoCtx.IDENT() + if identNode is None: + raise AGTypeError(ctx.getText(), "Missing type annotation identifier") + anno = identNode.getText() + if valueCtx is None: + raise AGTypeError(ctx.getText(), "Missing value for annotated type") + return self.handleAnnotatedValue(anno, valueCtx) + else: + if valueCtx is None: + return None + return valueCtx.accept(self) + + + @staticmethod + def _stripStringDelimiters(stringToken): + # The Agtype STRING token follows the JSON string grammar exactly + # (surrounding '"' plus \-escapes incl. \uXXXX), so json.loads both + # strips the delimiters and decodes the escapes — slicing alone would + # return 'a\\nb' as a literal backslash-n. + return json.loads(stringToken.getText()) + + # Visit a parse tree produced by AgtypeParser#StringValue. + def visitStringValue(self, ctx:AgtypeParser.StringValueContext): + return self._stripStringDelimiters(ctx.STRING()) + + + # Visit a parse tree produced by AgtypeParser#IntegerValue. + def visitIntegerValue(self, ctx:AgtypeParser.IntegerValueContext): + return int(ctx.INTEGER().getText()) + + # Visit a parse tree produced by AgtypeParser#floatLiteral. + def visitFloatLiteral(self, ctx:AgtypeParser.FloatLiteralContext): + text = ctx.getText() + c = ctx.getChild(0) + if c is None or not hasattr(c, 'symbol') or c.symbol is None: + raise AGTypeError( + str(text), + "Malformed float literal: missing or invalid child node" + ) + tp = c.symbol.type + if tp == AgtypeParser.RegularFloat: + return float(text) + elif tp == AgtypeParser.ExponentFloat: + return float(text) + else: + if text == 'NaN': + return float('nan') + elif text == '-Infinity': + return float('-inf') + elif text == 'Infinity': + return float('inf') + else: + raise ValueError("Unknown float expression: " + text) + + + # Visit a parse tree produced by AgtypeParser#TrueBoolean. + def visitTrueBoolean(self, ctx:AgtypeParser.TrueBooleanContext): + return True + + + # Visit a parse tree produced by AgtypeParser#FalseBoolean. + def visitFalseBoolean(self, ctx:AgtypeParser.FalseBooleanContext): + return False + + + # Visit a parse tree produced by AgtypeParser#NullValue. + def visitNullValue(self, ctx:AgtypeParser.NullValueContext): + return None + + + # Visit a parse tree produced by AgtypeParser#obj. + def visitObj(self, ctx:AgtypeParser.ObjContext): + obj = dict() + for c in ctx.getChildren(): + if isinstance(c, AgtypeParser.PairContext): + namVal = self.visitPair(c) + name = namVal[0] + valCtx = namVal[1] + # visitPair() raises AGTypeError when the value node is + # missing, so valCtx should never be None here. The + # guard is kept as a defensive fallback only. + if valCtx is not None: + val = valCtx.accept(self) + obj[name] = val + else: + obj[name] = None + return obj + + + # Visit a parse tree produced by AgtypeParser#pair. + def visitPair(self, ctx:AgtypeParser.PairContext): + self.visitChildren(ctx) + strNode = ctx.STRING() + agValNode = ctx.agValue() + if strNode is None: + raise AGTypeError(ctx.getText(), "Missing key in object pair") + if agValNode is None: + raise AGTypeError(ctx.getText(), "Missing value in object pair") + return (self._stripStringDelimiters(strNode), agValNode) + + + # Visit a parse tree produced by AgtypeParser#array. + def visitArray(self, ctx:AgtypeParser.ArrayContext): + li = list() + for c in ctx.getChildren(): + if not isinstance(c, TerminalNode): + val = c.accept(self) + li.append(val) + return li + + def handleAnnotatedValue(self, anno:str, ctx:ParserRuleContext): + # Each branch below constructs a model object (Vertex, Edge, Path) + # and populates it from the parsed dict/list. If a type check + # fails (e.g. the parsed value is not a dict), AGTypeError is + # raised and the partially-constructed object is discarded — no + # cleanup is needed because the caller propagates the exception. + if anno == "numeric": + return Decimal(ctx.getText()) + elif anno == "vertex": + d = ctx.accept(self) + if not isinstance(d, dict): + raise AGTypeError(str(ctx.getText()), "Expected dict for vertex, got " + type(d).__name__) + vid = d.get("id") + vertex = None + if self.vertexCache is not None and vid in self.vertexCache: + vertex = self.vertexCache[vid] + else: + vertex = Vertex() + vertex.id = d.get("id") + vertex.label = d.get("label") + vertex.properties = d.get("properties") or {} + + if self.vertexCache is not None: + self.vertexCache[vid] = vertex + + return vertex + + elif anno == "edge": + edge = Edge() + d = ctx.accept(self) + if not isinstance(d, dict): + raise AGTypeError(str(ctx.getText()), "Expected dict for edge, got " + type(d).__name__) + edge.id = d.get("id") + edge.label = d.get("label") + edge.end_id = d.get("end_id") + edge.start_id = d.get("start_id") + edge.properties = d.get("properties") or {} + + return edge + + elif anno == "path": + arr = ctx.accept(self) + if not isinstance(arr, list): + raise AGTypeError(str(ctx.getText()), "Expected list for path, got " + type(arr).__name__) + path = Path(arr) + + return path + + return ctx.accept(self) diff --git a/packages/ai/src/ai/common/graph/age/_agtype/exceptions.py b/packages/ai/src/ai/common/graph/age/_agtype/exceptions.py new file mode 100644 index 000000000..f17b6a3c2 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_agtype/exceptions.py @@ -0,0 +1,101 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + + +class AgeNotSet(Exception): + def __init__(self, name=None): + self.name = name + super().__init__(name or 'AGE extension is not set.') + + def __repr__(self): + return 'AGE extension is not set.' + +class GraphNotFound(Exception): + def __init__(self, name=None): + self.name = name + super().__init__(f'Graph[{name}] does not exist.' if name else 'Graph does not exist.') + + def __repr__(self): + if self.name: + return 'Graph[' + self.name + '] does not exist.' + return 'Graph does not exist.' + + +class GraphAlreadyExists(Exception): + def __init__(self, name=None): + self.name = name + super().__init__(f'Graph[{name}] already exists.' if name else 'Graph already exists.') + + def __repr__(self): + if self.name: + return 'Graph[' + self.name + '] already exists.' + return 'Graph already exists.' + + +class InvalidGraphName(Exception): + """Raised when a graph name contains invalid characters.""" + def __init__(self, name, reason=None): + self.name = name + self.reason = reason + msg = f"Invalid graph name: '{name}'." + if reason: + msg += f" {reason}" + super().__init__(msg) + + def __repr__(self): + return f"InvalidGraphName('{self.name}')" + + +class InvalidIdentifier(Exception): + """Raised when an identifier (column, label, etc.) is invalid.""" + def __init__(self, name, context=None): + self.name = name + self.context = context + msg = f"Invalid identifier: '{name}'." + if context: + msg += f" {context}" + super().__init__(msg) + + def __repr__(self): + return f"InvalidIdentifier('{self.name}')" + + +class GraphNotSet(Exception): + def __repr__(self): + return 'Graph name is not set.' + + +class NoConnection(Exception): + def __repr__(self): + return 'No Connection' + +class NoCursor(Exception): + def __repr__(self): + return 'No Cursor' + +class SqlExecutionError(Exception): + def __init__(self, msg, cause): + self.msg = msg + self.cause = cause + super().__init__(msg, cause) + + def __repr__(self): + return 'SqlExecution [' + self.msg + ']' + +class AGTypeError(Exception): + def __init__(self, msg, cause): + self.msg = msg + self.cause = cause + super().__init__(msg, cause) diff --git a/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeLexer.py b/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeLexer.py new file mode 100644 index 000000000..71b0851cc --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeLexer.py @@ -0,0 +1,135 @@ +# Generated from Agtype.g4 by ANTLR 4.13.2 +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + + +def serializedATN(): + return [ + 4,0,19,183,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5, + 2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2, + 13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7, + 19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,1, + 0,1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1, + 3,1,3,1,4,1,4,1,5,1,5,1,6,1,6,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,10,1, + 10,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,12,1, + 12,1,13,1,13,5,13,102,8,13,10,13,12,13,105,9,13,1,14,1,14,1,14,5, + 14,110,8,14,10,14,12,14,113,9,14,1,14,1,14,1,15,1,15,1,15,3,15,120, + 8,15,1,16,1,16,1,16,1,16,1,16,1,16,1,17,1,17,1,18,1,18,1,19,3,19, + 133,8,19,1,19,1,19,1,20,1,20,1,20,5,20,140,8,20,10,20,12,20,143, + 9,20,3,20,145,8,20,1,21,3,21,148,8,21,1,21,1,21,1,21,1,22,3,22,154, + 8,22,1,22,1,22,3,22,158,8,22,1,22,1,22,1,23,1,23,4,23,164,8,23,11, + 23,12,23,165,1,24,1,24,3,24,170,8,24,1,24,4,24,173,8,24,11,24,12, + 24,174,1,25,4,25,178,8,25,11,25,12,25,179,1,25,1,25,0,0,26,1,1,3, + 2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14, + 29,15,31,0,33,0,35,0,37,0,39,16,41,0,43,17,45,18,47,0,49,0,51,19, + 1,0,10,3,0,65,90,95,95,97,122,5,0,36,36,48,57,65,90,95,95,97,122, + 8,0,34,34,47,47,92,92,98,98,102,102,110,110,114,114,116,116,3,0, + 48,57,65,70,97,102,3,0,0,31,34,34,92,92,1,0,49,57,1,0,48,57,2,0, + 69,69,101,101,2,0,43,43,45,45,3,0,9,10,13,13,32,32,189,0,1,1,0,0, + 0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0, + 13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0, + 23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,39,1,0,0,0,0, + 43,1,0,0,0,0,45,1,0,0,0,0,51,1,0,0,0,1,53,1,0,0,0,3,58,1,0,0,0,5, + 64,1,0,0,0,7,69,1,0,0,0,9,71,1,0,0,0,11,73,1,0,0,0,13,75,1,0,0,0, + 15,77,1,0,0,0,17,79,1,0,0,0,19,81,1,0,0,0,21,84,1,0,0,0,23,86,1, + 0,0,0,25,95,1,0,0,0,27,99,1,0,0,0,29,106,1,0,0,0,31,116,1,0,0,0, + 33,121,1,0,0,0,35,127,1,0,0,0,37,129,1,0,0,0,39,132,1,0,0,0,41,144, + 1,0,0,0,43,147,1,0,0,0,45,153,1,0,0,0,47,161,1,0,0,0,49,167,1,0, + 0,0,51,177,1,0,0,0,53,54,5,116,0,0,54,55,5,114,0,0,55,56,5,117,0, + 0,56,57,5,101,0,0,57,2,1,0,0,0,58,59,5,102,0,0,59,60,5,97,0,0,60, + 61,5,108,0,0,61,62,5,115,0,0,62,63,5,101,0,0,63,4,1,0,0,0,64,65, + 5,110,0,0,65,66,5,117,0,0,66,67,5,108,0,0,67,68,5,108,0,0,68,6,1, + 0,0,0,69,70,5,123,0,0,70,8,1,0,0,0,71,72,5,44,0,0,72,10,1,0,0,0, + 73,74,5,125,0,0,74,12,1,0,0,0,75,76,5,58,0,0,76,14,1,0,0,0,77,78, + 5,91,0,0,78,16,1,0,0,0,79,80,5,93,0,0,80,18,1,0,0,0,81,82,5,58,0, + 0,82,83,5,58,0,0,83,20,1,0,0,0,84,85,5,45,0,0,85,22,1,0,0,0,86,87, + 5,73,0,0,87,88,5,110,0,0,88,89,5,102,0,0,89,90,5,105,0,0,90,91,5, + 110,0,0,91,92,5,105,0,0,92,93,5,116,0,0,93,94,5,121,0,0,94,24,1, + 0,0,0,95,96,5,78,0,0,96,97,5,97,0,0,97,98,5,78,0,0,98,26,1,0,0,0, + 99,103,7,0,0,0,100,102,7,1,0,0,101,100,1,0,0,0,102,105,1,0,0,0,103, + 101,1,0,0,0,103,104,1,0,0,0,104,28,1,0,0,0,105,103,1,0,0,0,106,111, + 5,34,0,0,107,110,3,31,15,0,108,110,3,37,18,0,109,107,1,0,0,0,109, + 108,1,0,0,0,110,113,1,0,0,0,111,109,1,0,0,0,111,112,1,0,0,0,112, + 114,1,0,0,0,113,111,1,0,0,0,114,115,5,34,0,0,115,30,1,0,0,0,116, + 119,5,92,0,0,117,120,7,2,0,0,118,120,3,33,16,0,119,117,1,0,0,0,119, + 118,1,0,0,0,120,32,1,0,0,0,121,122,5,117,0,0,122,123,3,35,17,0,123, + 124,3,35,17,0,124,125,3,35,17,0,125,126,3,35,17,0,126,34,1,0,0,0, + 127,128,7,3,0,0,128,36,1,0,0,0,129,130,8,4,0,0,130,38,1,0,0,0,131, + 133,5,45,0,0,132,131,1,0,0,0,132,133,1,0,0,0,133,134,1,0,0,0,134, + 135,3,41,20,0,135,40,1,0,0,0,136,145,5,48,0,0,137,141,7,5,0,0,138, + 140,7,6,0,0,139,138,1,0,0,0,140,143,1,0,0,0,141,139,1,0,0,0,141, + 142,1,0,0,0,142,145,1,0,0,0,143,141,1,0,0,0,144,136,1,0,0,0,144, + 137,1,0,0,0,145,42,1,0,0,0,146,148,5,45,0,0,147,146,1,0,0,0,147, + 148,1,0,0,0,148,149,1,0,0,0,149,150,3,41,20,0,150,151,3,47,23,0, + 151,44,1,0,0,0,152,154,5,45,0,0,153,152,1,0,0,0,153,154,1,0,0,0, + 154,155,1,0,0,0,155,157,3,41,20,0,156,158,3,47,23,0,157,156,1,0, + 0,0,157,158,1,0,0,0,158,159,1,0,0,0,159,160,3,49,24,0,160,46,1,0, + 0,0,161,163,5,46,0,0,162,164,7,6,0,0,163,162,1,0,0,0,164,165,1,0, + 0,0,165,163,1,0,0,0,165,166,1,0,0,0,166,48,1,0,0,0,167,169,7,7,0, + 0,168,170,7,8,0,0,169,168,1,0,0,0,169,170,1,0,0,0,170,172,1,0,0, + 0,171,173,7,6,0,0,172,171,1,0,0,0,173,174,1,0,0,0,174,172,1,0,0, + 0,174,175,1,0,0,0,175,50,1,0,0,0,176,178,7,9,0,0,177,176,1,0,0,0, + 178,179,1,0,0,0,179,177,1,0,0,0,179,180,1,0,0,0,180,181,1,0,0,0, + 181,182,6,25,0,0,182,52,1,0,0,0,15,0,103,109,111,119,132,141,144, + 147,153,157,165,169,174,179,1,6,0,0 + ] + +class AgtypeLexer(Lexer): + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + T__0 = 1 + T__1 = 2 + T__2 = 3 + T__3 = 4 + T__4 = 5 + T__5 = 6 + T__6 = 7 + T__7 = 8 + T__8 = 9 + T__9 = 10 + T__10 = 11 + T__11 = 12 + T__12 = 13 + IDENT = 14 + STRING = 15 + INTEGER = 16 + RegularFloat = 17 + ExponentFloat = 18 + WS = 19 + + channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ] + + modeNames = [ "DEFAULT_MODE" ] + + literalNames = [ "", + "'true'", "'false'", "'null'", "'{'", "','", "'}'", "':'", "'['", + "']'", "'::'", "'-'", "'Infinity'", "'NaN'" ] + + symbolicNames = [ "", + "IDENT", "STRING", "INTEGER", "RegularFloat", "ExponentFloat", + "WS" ] + + ruleNames = [ "T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6", + "T__7", "T__8", "T__9", "T__10", "T__11", "T__12", "IDENT", + "STRING", "ESC", "UNICODE", "HEX", "SAFECODEPOINT", "INTEGER", + "INT", "RegularFloat", "ExponentFloat", "DECIMAL", "SCIENTIFIC", + "WS" ] + + grammarFileName = "Agtype.g4" + + def __init__(self, input=None, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.13.2") + self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache()) + self._actions = None + self._predicates = None + + diff --git a/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeListener.py b/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeListener.py new file mode 100644 index 000000000..982dfaaf5 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeListener.py @@ -0,0 +1,147 @@ +# Generated from Agtype.g4 by ANTLR 4.13.2 +from antlr4 import * +if "." in __name__: + from .AgtypeParser import AgtypeParser +else: + from AgtypeParser import AgtypeParser + +# This class defines a complete listener for a parse tree produced by AgtypeParser. +class AgtypeListener(ParseTreeListener): + + # Enter a parse tree produced by AgtypeParser#agType. + def enterAgType(self, ctx:AgtypeParser.AgTypeContext): + pass + + # Exit a parse tree produced by AgtypeParser#agType. + def exitAgType(self, ctx:AgtypeParser.AgTypeContext): + pass + + + # Enter a parse tree produced by AgtypeParser#agValue. + def enterAgValue(self, ctx:AgtypeParser.AgValueContext): + pass + + # Exit a parse tree produced by AgtypeParser#agValue. + def exitAgValue(self, ctx:AgtypeParser.AgValueContext): + pass + + + # Enter a parse tree produced by AgtypeParser#StringValue. + def enterStringValue(self, ctx:AgtypeParser.StringValueContext): + pass + + # Exit a parse tree produced by AgtypeParser#StringValue. + def exitStringValue(self, ctx:AgtypeParser.StringValueContext): + pass + + + # Enter a parse tree produced by AgtypeParser#IntegerValue. + def enterIntegerValue(self, ctx:AgtypeParser.IntegerValueContext): + pass + + # Exit a parse tree produced by AgtypeParser#IntegerValue. + def exitIntegerValue(self, ctx:AgtypeParser.IntegerValueContext): + pass + + + # Enter a parse tree produced by AgtypeParser#FloatValue. + def enterFloatValue(self, ctx:AgtypeParser.FloatValueContext): + pass + + # Exit a parse tree produced by AgtypeParser#FloatValue. + def exitFloatValue(self, ctx:AgtypeParser.FloatValueContext): + pass + + + # Enter a parse tree produced by AgtypeParser#TrueBoolean. + def enterTrueBoolean(self, ctx:AgtypeParser.TrueBooleanContext): + pass + + # Exit a parse tree produced by AgtypeParser#TrueBoolean. + def exitTrueBoolean(self, ctx:AgtypeParser.TrueBooleanContext): + pass + + + # Enter a parse tree produced by AgtypeParser#FalseBoolean. + def enterFalseBoolean(self, ctx:AgtypeParser.FalseBooleanContext): + pass + + # Exit a parse tree produced by AgtypeParser#FalseBoolean. + def exitFalseBoolean(self, ctx:AgtypeParser.FalseBooleanContext): + pass + + + # Enter a parse tree produced by AgtypeParser#NullValue. + def enterNullValue(self, ctx:AgtypeParser.NullValueContext): + pass + + # Exit a parse tree produced by AgtypeParser#NullValue. + def exitNullValue(self, ctx:AgtypeParser.NullValueContext): + pass + + + # Enter a parse tree produced by AgtypeParser#ObjectValue. + def enterObjectValue(self, ctx:AgtypeParser.ObjectValueContext): + pass + + # Exit a parse tree produced by AgtypeParser#ObjectValue. + def exitObjectValue(self, ctx:AgtypeParser.ObjectValueContext): + pass + + + # Enter a parse tree produced by AgtypeParser#ArrayValue. + def enterArrayValue(self, ctx:AgtypeParser.ArrayValueContext): + pass + + # Exit a parse tree produced by AgtypeParser#ArrayValue. + def exitArrayValue(self, ctx:AgtypeParser.ArrayValueContext): + pass + + + # Enter a parse tree produced by AgtypeParser#obj. + def enterObj(self, ctx:AgtypeParser.ObjContext): + pass + + # Exit a parse tree produced by AgtypeParser#obj. + def exitObj(self, ctx:AgtypeParser.ObjContext): + pass + + + # Enter a parse tree produced by AgtypeParser#pair. + def enterPair(self, ctx:AgtypeParser.PairContext): + pass + + # Exit a parse tree produced by AgtypeParser#pair. + def exitPair(self, ctx:AgtypeParser.PairContext): + pass + + + # Enter a parse tree produced by AgtypeParser#array. + def enterArray(self, ctx:AgtypeParser.ArrayContext): + pass + + # Exit a parse tree produced by AgtypeParser#array. + def exitArray(self, ctx:AgtypeParser.ArrayContext): + pass + + + # Enter a parse tree produced by AgtypeParser#typeAnnotation. + def enterTypeAnnotation(self, ctx:AgtypeParser.TypeAnnotationContext): + pass + + # Exit a parse tree produced by AgtypeParser#typeAnnotation. + def exitTypeAnnotation(self, ctx:AgtypeParser.TypeAnnotationContext): + pass + + + # Enter a parse tree produced by AgtypeParser#floatLiteral. + def enterFloatLiteral(self, ctx:AgtypeParser.FloatLiteralContext): + pass + + # Exit a parse tree produced by AgtypeParser#floatLiteral. + def exitFloatLiteral(self, ctx:AgtypeParser.FloatLiteralContext): + pass + + + +del AgtypeParser \ No newline at end of file diff --git a/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeParser.py b/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeParser.py new file mode 100644 index 000000000..e34c60f5f --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeParser.py @@ -0,0 +1,846 @@ +# Generated from Agtype.g4 by ANTLR 4.13.2 +# encoding: utf-8 +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + +def serializedATN(): + return [ + 4,1,19,80,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7, + 6,2,7,7,7,1,0,1,0,1,0,1,1,1,1,3,1,22,8,1,1,2,1,2,1,2,1,2,1,2,1,2, + 1,2,1,2,3,2,32,8,2,1,3,1,3,1,3,1,3,5,3,38,8,3,10,3,12,3,41,9,3,1, + 3,1,3,1,3,1,3,3,3,47,8,3,1,4,1,4,1,4,1,4,1,5,1,5,1,5,1,5,5,5,57, + 8,5,10,5,12,5,60,9,5,1,5,1,5,1,5,1,5,3,5,66,8,5,1,6,1,6,1,6,1,7, + 1,7,1,7,3,7,74,8,7,1,7,1,7,3,7,78,8,7,1,7,0,0,8,0,2,4,6,8,10,12, + 14,0,0,87,0,16,1,0,0,0,2,19,1,0,0,0,4,31,1,0,0,0,6,46,1,0,0,0,8, + 48,1,0,0,0,10,65,1,0,0,0,12,67,1,0,0,0,14,77,1,0,0,0,16,17,3,2,1, + 0,17,18,5,0,0,1,18,1,1,0,0,0,19,21,3,4,2,0,20,22,3,12,6,0,21,20, + 1,0,0,0,21,22,1,0,0,0,22,3,1,0,0,0,23,32,5,15,0,0,24,32,5,16,0,0, + 25,32,3,14,7,0,26,32,5,1,0,0,27,32,5,2,0,0,28,32,5,3,0,0,29,32,3, + 6,3,0,30,32,3,10,5,0,31,23,1,0,0,0,31,24,1,0,0,0,31,25,1,0,0,0,31, + 26,1,0,0,0,31,27,1,0,0,0,31,28,1,0,0,0,31,29,1,0,0,0,31,30,1,0,0, + 0,32,5,1,0,0,0,33,34,5,4,0,0,34,39,3,8,4,0,35,36,5,5,0,0,36,38,3, + 8,4,0,37,35,1,0,0,0,38,41,1,0,0,0,39,37,1,0,0,0,39,40,1,0,0,0,40, + 42,1,0,0,0,41,39,1,0,0,0,42,43,5,6,0,0,43,47,1,0,0,0,44,45,5,4,0, + 0,45,47,5,6,0,0,46,33,1,0,0,0,46,44,1,0,0,0,47,7,1,0,0,0,48,49,5, + 15,0,0,49,50,5,7,0,0,50,51,3,2,1,0,51,9,1,0,0,0,52,53,5,8,0,0,53, + 58,3,2,1,0,54,55,5,5,0,0,55,57,3,2,1,0,56,54,1,0,0,0,57,60,1,0,0, + 0,58,56,1,0,0,0,58,59,1,0,0,0,59,61,1,0,0,0,60,58,1,0,0,0,61,62, + 5,9,0,0,62,66,1,0,0,0,63,64,5,8,0,0,64,66,5,9,0,0,65,52,1,0,0,0, + 65,63,1,0,0,0,66,11,1,0,0,0,67,68,5,10,0,0,68,69,5,14,0,0,69,13, + 1,0,0,0,70,78,5,17,0,0,71,78,5,18,0,0,72,74,5,11,0,0,73,72,1,0,0, + 0,73,74,1,0,0,0,74,75,1,0,0,0,75,78,5,12,0,0,76,78,5,13,0,0,77,70, + 1,0,0,0,77,71,1,0,0,0,77,73,1,0,0,0,77,76,1,0,0,0,78,15,1,0,0,0, + 8,21,31,39,46,58,65,73,77 + ] + +class AgtypeParser ( Parser ): + + grammarFileName = "Agtype.g4" + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + sharedContextCache = PredictionContextCache() + + literalNames = [ "", "'true'", "'false'", "'null'", "'{'", + "','", "'}'", "':'", "'['", "']'", "'::'", "'-'", "'Infinity'", + "'NaN'" ] + + symbolicNames = [ "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "IDENT", "STRING", "INTEGER", + "RegularFloat", "ExponentFloat", "WS" ] + + RULE_agType = 0 + RULE_agValue = 1 + RULE_value = 2 + RULE_obj = 3 + RULE_pair = 4 + RULE_array = 5 + RULE_typeAnnotation = 6 + RULE_floatLiteral = 7 + + ruleNames = [ "agType", "agValue", "value", "obj", "pair", "array", + "typeAnnotation", "floatLiteral" ] + + EOF = Token.EOF + T__0=1 + T__1=2 + T__2=3 + T__3=4 + T__4=5 + T__5=6 + T__6=7 + T__7=8 + T__8=9 + T__9=10 + T__10=11 + T__11=12 + T__12=13 + IDENT=14 + STRING=15 + INTEGER=16 + RegularFloat=17 + ExponentFloat=18 + WS=19 + + def __init__(self, input:TokenStream, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.13.2") + self._interp = ParserATNSimulator(self, self.atn, self.decisionsToDFA, self.sharedContextCache) + self._predicates = None + + + + + class AgTypeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def agValue(self): + return self.getTypedRuleContext(AgtypeParser.AgValueContext,0) + + + def EOF(self): + return self.getToken(AgtypeParser.EOF, 0) + + def getRuleIndex(self): + return AgtypeParser.RULE_agType + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAgType" ): + listener.enterAgType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAgType" ): + listener.exitAgType(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAgType" ): + return visitor.visitAgType(self) + else: + return visitor.visitChildren(self) + + + + + def agType(self): + + localctx = AgtypeParser.AgTypeContext(self, self._ctx, self.state) + self.enterRule(localctx, 0, self.RULE_agType) + try: + self.enterOuterAlt(localctx, 1) + self.state = 16 + self.agValue() + self.state = 17 + self.match(AgtypeParser.EOF) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class AgValueContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def value(self): + return self.getTypedRuleContext(AgtypeParser.ValueContext,0) + + + def typeAnnotation(self): + return self.getTypedRuleContext(AgtypeParser.TypeAnnotationContext,0) + + + def getRuleIndex(self): + return AgtypeParser.RULE_agValue + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAgValue" ): + listener.enterAgValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAgValue" ): + listener.exitAgValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAgValue" ): + return visitor.visitAgValue(self) + else: + return visitor.visitChildren(self) + + + + + def agValue(self): + + localctx = AgtypeParser.AgValueContext(self, self._ctx, self.state) + self.enterRule(localctx, 2, self.RULE_agValue) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 19 + self.value() + self.state = 21 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==10: + self.state = 20 + self.typeAnnotation() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ValueContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return AgtypeParser.RULE_value + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class NullValueContext(ValueContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AgtypeParser.ValueContext + super().__init__(parser) + self.copyFrom(ctx) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNullValue" ): + listener.enterNullValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNullValue" ): + listener.exitNullValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNullValue" ): + return visitor.visitNullValue(self) + else: + return visitor.visitChildren(self) + + + class ObjectValueContext(ValueContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AgtypeParser.ValueContext + super().__init__(parser) + self.copyFrom(ctx) + + def obj(self): + return self.getTypedRuleContext(AgtypeParser.ObjContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterObjectValue" ): + listener.enterObjectValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitObjectValue" ): + listener.exitObjectValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitObjectValue" ): + return visitor.visitObjectValue(self) + else: + return visitor.visitChildren(self) + + + class IntegerValueContext(ValueContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AgtypeParser.ValueContext + super().__init__(parser) + self.copyFrom(ctx) + + def INTEGER(self): + return self.getToken(AgtypeParser.INTEGER, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIntegerValue" ): + listener.enterIntegerValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIntegerValue" ): + listener.exitIntegerValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitIntegerValue" ): + return visitor.visitIntegerValue(self) + else: + return visitor.visitChildren(self) + + + class TrueBooleanContext(ValueContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AgtypeParser.ValueContext + super().__init__(parser) + self.copyFrom(ctx) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTrueBoolean" ): + listener.enterTrueBoolean(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTrueBoolean" ): + listener.exitTrueBoolean(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTrueBoolean" ): + return visitor.visitTrueBoolean(self) + else: + return visitor.visitChildren(self) + + + class FalseBooleanContext(ValueContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AgtypeParser.ValueContext + super().__init__(parser) + self.copyFrom(ctx) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFalseBoolean" ): + listener.enterFalseBoolean(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFalseBoolean" ): + listener.exitFalseBoolean(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFalseBoolean" ): + return visitor.visitFalseBoolean(self) + else: + return visitor.visitChildren(self) + + + class FloatValueContext(ValueContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AgtypeParser.ValueContext + super().__init__(parser) + self.copyFrom(ctx) + + def floatLiteral(self): + return self.getTypedRuleContext(AgtypeParser.FloatLiteralContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFloatValue" ): + listener.enterFloatValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFloatValue" ): + listener.exitFloatValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFloatValue" ): + return visitor.visitFloatValue(self) + else: + return visitor.visitChildren(self) + + + class StringValueContext(ValueContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AgtypeParser.ValueContext + super().__init__(parser) + self.copyFrom(ctx) + + def STRING(self): + return self.getToken(AgtypeParser.STRING, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterStringValue" ): + listener.enterStringValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitStringValue" ): + listener.exitStringValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitStringValue" ): + return visitor.visitStringValue(self) + else: + return visitor.visitChildren(self) + + + class ArrayValueContext(ValueContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a AgtypeParser.ValueContext + super().__init__(parser) + self.copyFrom(ctx) + + def array(self): + return self.getTypedRuleContext(AgtypeParser.ArrayContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterArrayValue" ): + listener.enterArrayValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitArrayValue" ): + listener.exitArrayValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitArrayValue" ): + return visitor.visitArrayValue(self) + else: + return visitor.visitChildren(self) + + + + def value(self): + + localctx = AgtypeParser.ValueContext(self, self._ctx, self.state) + self.enterRule(localctx, 4, self.RULE_value) + try: + self.state = 31 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [15]: + localctx = AgtypeParser.StringValueContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 23 + self.match(AgtypeParser.STRING) + pass + elif token in [16]: + localctx = AgtypeParser.IntegerValueContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 24 + self.match(AgtypeParser.INTEGER) + pass + elif token in [11, 12, 13, 17, 18]: + localctx = AgtypeParser.FloatValueContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 25 + self.floatLiteral() + pass + elif token in [1]: + localctx = AgtypeParser.TrueBooleanContext(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 26 + self.match(AgtypeParser.T__0) + pass + elif token in [2]: + localctx = AgtypeParser.FalseBooleanContext(self, localctx) + self.enterOuterAlt(localctx, 5) + self.state = 27 + self.match(AgtypeParser.T__1) + pass + elif token in [3]: + localctx = AgtypeParser.NullValueContext(self, localctx) + self.enterOuterAlt(localctx, 6) + self.state = 28 + self.match(AgtypeParser.T__2) + pass + elif token in [4]: + localctx = AgtypeParser.ObjectValueContext(self, localctx) + self.enterOuterAlt(localctx, 7) + self.state = 29 + self.obj() + pass + elif token in [8]: + localctx = AgtypeParser.ArrayValueContext(self, localctx) + self.enterOuterAlt(localctx, 8) + self.state = 30 + self.array() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ObjContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def pair(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AgtypeParser.PairContext) + else: + return self.getTypedRuleContext(AgtypeParser.PairContext,i) + + + def getRuleIndex(self): + return AgtypeParser.RULE_obj + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterObj" ): + listener.enterObj(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitObj" ): + listener.exitObj(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitObj" ): + return visitor.visitObj(self) + else: + return visitor.visitChildren(self) + + + + + def obj(self): + + localctx = AgtypeParser.ObjContext(self, self._ctx, self.state) + self.enterRule(localctx, 6, self.RULE_obj) + self._la = 0 # Token type + try: + self.state = 46 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,3,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 33 + self.match(AgtypeParser.T__3) + self.state = 34 + self.pair() + self.state = 39 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==5: + self.state = 35 + self.match(AgtypeParser.T__4) + self.state = 36 + self.pair() + self.state = 41 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 42 + self.match(AgtypeParser.T__5) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 44 + self.match(AgtypeParser.T__3) + self.state = 45 + self.match(AgtypeParser.T__5) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PairContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def STRING(self): + return self.getToken(AgtypeParser.STRING, 0) + + def agValue(self): + return self.getTypedRuleContext(AgtypeParser.AgValueContext,0) + + + def getRuleIndex(self): + return AgtypeParser.RULE_pair + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPair" ): + listener.enterPair(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPair" ): + listener.exitPair(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPair" ): + return visitor.visitPair(self) + else: + return visitor.visitChildren(self) + + + + + def pair(self): + + localctx = AgtypeParser.PairContext(self, self._ctx, self.state) + self.enterRule(localctx, 8, self.RULE_pair) + try: + self.enterOuterAlt(localctx, 1) + self.state = 48 + self.match(AgtypeParser.STRING) + self.state = 49 + self.match(AgtypeParser.T__6) + self.state = 50 + self.agValue() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ArrayContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def agValue(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(AgtypeParser.AgValueContext) + else: + return self.getTypedRuleContext(AgtypeParser.AgValueContext,i) + + + def getRuleIndex(self): + return AgtypeParser.RULE_array + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterArray" ): + listener.enterArray(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitArray" ): + listener.exitArray(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitArray" ): + return visitor.visitArray(self) + else: + return visitor.visitChildren(self) + + + + + def array(self): + + localctx = AgtypeParser.ArrayContext(self, self._ctx, self.state) + self.enterRule(localctx, 10, self.RULE_array) + self._la = 0 # Token type + try: + self.state = 65 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,5,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 52 + self.match(AgtypeParser.T__7) + self.state = 53 + self.agValue() + self.state = 58 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==5: + self.state = 54 + self.match(AgtypeParser.T__4) + self.state = 55 + self.agValue() + self.state = 60 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 61 + self.match(AgtypeParser.T__8) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 63 + self.match(AgtypeParser.T__7) + self.state = 64 + self.match(AgtypeParser.T__8) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class TypeAnnotationContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def IDENT(self): + return self.getToken(AgtypeParser.IDENT, 0) + + def getRuleIndex(self): + return AgtypeParser.RULE_typeAnnotation + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTypeAnnotation" ): + listener.enterTypeAnnotation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTypeAnnotation" ): + listener.exitTypeAnnotation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTypeAnnotation" ): + return visitor.visitTypeAnnotation(self) + else: + return visitor.visitChildren(self) + + + + + def typeAnnotation(self): + + localctx = AgtypeParser.TypeAnnotationContext(self, self._ctx, self.state) + self.enterRule(localctx, 12, self.RULE_typeAnnotation) + try: + self.enterOuterAlt(localctx, 1) + self.state = 67 + self.match(AgtypeParser.T__9) + self.state = 68 + self.match(AgtypeParser.IDENT) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FloatLiteralContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def RegularFloat(self): + return self.getToken(AgtypeParser.RegularFloat, 0) + + def ExponentFloat(self): + return self.getToken(AgtypeParser.ExponentFloat, 0) + + def getRuleIndex(self): + return AgtypeParser.RULE_floatLiteral + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFloatLiteral" ): + listener.enterFloatLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFloatLiteral" ): + listener.exitFloatLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFloatLiteral" ): + return visitor.visitFloatLiteral(self) + else: + return visitor.visitChildren(self) + + + + + def floatLiteral(self): + + localctx = AgtypeParser.FloatLiteralContext(self, self._ctx, self.state) + self.enterRule(localctx, 14, self.RULE_floatLiteral) + self._la = 0 # Token type + try: + self.state = 77 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [17]: + self.enterOuterAlt(localctx, 1) + self.state = 70 + self.match(AgtypeParser.RegularFloat) + pass + elif token in [18]: + self.enterOuterAlt(localctx, 2) + self.state = 71 + self.match(AgtypeParser.ExponentFloat) + pass + elif token in [11, 12]: + self.enterOuterAlt(localctx, 3) + self.state = 73 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==11: + self.state = 72 + self.match(AgtypeParser.T__10) + + + self.state = 75 + self.match(AgtypeParser.T__11) + pass + elif token in [13]: + self.enterOuterAlt(localctx, 4) + self.state = 76 + self.match(AgtypeParser.T__12) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + + + diff --git a/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeVisitor.py b/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeVisitor.py new file mode 100644 index 000000000..9d81ef974 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_agtype/gen/AgtypeVisitor.py @@ -0,0 +1,88 @@ +# Generated from Agtype.g4 by ANTLR 4.13.2 +from antlr4 import * +if "." in __name__: + from .AgtypeParser import AgtypeParser +else: + from AgtypeParser import AgtypeParser + +# This class defines a complete generic visitor for a parse tree produced by AgtypeParser. + +class AgtypeVisitor(ParseTreeVisitor): + + # Visit a parse tree produced by AgtypeParser#agType. + def visitAgType(self, ctx:AgtypeParser.AgTypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#agValue. + def visitAgValue(self, ctx:AgtypeParser.AgValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#StringValue. + def visitStringValue(self, ctx:AgtypeParser.StringValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#IntegerValue. + def visitIntegerValue(self, ctx:AgtypeParser.IntegerValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#FloatValue. + def visitFloatValue(self, ctx:AgtypeParser.FloatValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#TrueBoolean. + def visitTrueBoolean(self, ctx:AgtypeParser.TrueBooleanContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#FalseBoolean. + def visitFalseBoolean(self, ctx:AgtypeParser.FalseBooleanContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#NullValue. + def visitNullValue(self, ctx:AgtypeParser.NullValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#ObjectValue. + def visitObjectValue(self, ctx:AgtypeParser.ObjectValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#ArrayValue. + def visitArrayValue(self, ctx:AgtypeParser.ArrayValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#obj. + def visitObj(self, ctx:AgtypeParser.ObjContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#pair. + def visitPair(self, ctx:AgtypeParser.PairContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#array. + def visitArray(self, ctx:AgtypeParser.ArrayContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#typeAnnotation. + def visitTypeAnnotation(self, ctx:AgtypeParser.TypeAnnotationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by AgtypeParser#floatLiteral. + def visitFloatLiteral(self, ctx:AgtypeParser.FloatLiteralContext): + return self.visitChildren(ctx) + + + +del AgtypeParser \ No newline at end of file diff --git a/packages/ai/src/ai/common/graph/age/_agtype/gen/__init__.py b/packages/ai/src/ai/common/graph/age/_agtype/gen/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/ai/src/ai/common/graph/age/_agtype/models.py b/packages/ai/src/ai/common/graph/age/_agtype/models.py new file mode 100644 index 000000000..62215c160 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_agtype/models.py @@ -0,0 +1,334 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +import json +from io import StringIO + + +TP_NONE = 0 +TP_VERTEX = 1 +TP_EDGE = 2 +TP_PATH = 3 + + +class Graph(): + def __init__(self, stmt=None) -> None: + self.statement = stmt + self.rows = list() + self.vertices = dict() + + def __iter__(self): + return self.rows.__iter__() + + def __len__(self): + return self.rows.__len__() + + def __getitem__(self,index): + return self.rows[index] + + def size(self): + return self.rows.__len__() + + def append(self, agObj): + self.rows.append(agObj) + + def getVertices(self): + return self.vertices + + def getVertex(self, id): + if id in self.vertices: + return self.vertices[id] + else: + return None + +class AGObj: + @property + def gtype(self): + return TP_NONE + + +class Path(AGObj): + entities = [] + def __init__(self, entities=None) -> None: + self.entities = entities + + @property + def gtype(self): + return TP_PATH + + def __iter__(self): + return self.entities.__iter__() + + def __len__(self): + return self.entities.__len__() + + def __getitem__(self,index): + return self.entities[index] + + def size(self): + return self.entities.__len__() + + def append(self, agObj:AGObj ): + self.entities.append(agObj) + + def __str__(self) -> str: + return self.toString() + + def __repr__(self) -> str: + return self.toString() + + def toString(self) -> str: + buf = StringIO() + buf.write("[") + max = len(self.entities) + idx = 0 + while idx < max: + if idx > 0: + buf.write(",") + self.entities[idx]._toString(buf) + idx += 1 + buf.write("]::PATH") + + return buf.getvalue() + + def toJson(self) -> str: + buf = StringIO() + buf.write("{\"gtype\": \"path\", \"elements\": [") + + max = len(self.entities) + idx = 0 + while idx < max: + if idx > 0: + buf.write(",") + self.entities[idx]._toJson(buf) + idx += 1 + buf.write("]}") + + return buf.getvalue() + + def to_dict(self) -> list: + # AGObj elements are recursively converted; JSON-native types + # (dict, list, str, int, float, bool, None) pass through unchanged. + # Non-serializable objects fall back to str() as a safety net. + result = [] + for e in (self.entities or []): + if isinstance(e, AGObj): + result.append(e.to_dict()) + elif isinstance(e, (dict, list, str, int, float, bool, type(None))): + result.append(e) + else: + result.append(str(e)) + return result + + + + +class Vertex(AGObj): + def __init__(self, id=None, label=None, properties=None) -> None: + self.id = id + self.label = label + self.properties = properties + + @property + def gtype(self): + return TP_VERTEX + + def __setitem__(self,name, value): + self.properties[name]=value + + def __getitem__(self,name): + if name in self.properties: + return self.properties[name] + else: + return None + + def __str__(self) -> str: + return self.toString() + + def __repr__(self) -> str: + return self.toString() + + def to_dict(self) -> dict: + """Return a plain dict suitable for JSON serialization. + + Properties are shallow-copied; nested mutable values will share + references with the original Vertex. + """ + return { + "id": self.id, + "label": self.label, + "properties": dict(self.properties) if self.properties else {}, + } + + def toString(self) -> str: + return nodeToString(self) + + def _toString(self, buf): + _nodeToString(self, buf) + + def toJson(self) -> str: + return nodeToJson(self) + + def _toJson(self, buf): + _nodeToJson(self, buf) + + +class Edge(AGObj): + def __init__(self, id=None, label=None, properties=None) -> None: + self.id = id + self.label = label + self.start_id = None + self.end_id = None + self.properties = properties + + @property + def gtype(self): + return TP_EDGE + + def __setitem__(self,name, value): + self.properties[name]=value + + def __getitem__(self,name): + if name in self.properties: + return self.properties[name] + else: + return None + + def __str__(self) -> str: + return self.toString() + + def __repr__(self) -> str: + return self.toString() + + def to_dict(self) -> dict: + """Return a plain dict suitable for JSON serialization. + + Properties are shallow-copied; nested mutable values will share + references with the original Edge. + """ + return { + "id": self.id, + "label": self.label, + "start_id": self.start_id, + "end_id": self.end_id, + "properties": dict(self.properties) if self.properties else {}, + } + + def extraStrFormat(node, buf): + if node.start_id != None: + buf.write(", start_id:") + buf.write(str(node.start_id)) + + if node.end_id != None: + buf.write(", end_id:") + buf.write(str(node.end_id)) + + + def toString(self) -> str: + return nodeToString(self, Edge.extraStrFormat) + + def _toString(self, buf): + _nodeToString(self, buf, Edge.extraStrFormat) + + def extraJsonFormat(node, buf): + if node.start_id != None: + buf.write(", \"start_id\": \"") + buf.write(str(node.start_id)) + buf.write("\"") + + if node.end_id != None: + buf.write(", \"end_id\": \"") + buf.write(str(node.end_id)) + buf.write("\"") + + def toJson(self) -> str: + return nodeToJson(self, Edge.extraJsonFormat) + + def _toJson(self, buf): + _nodeToJson(self, buf, Edge.extraJsonFormat) + + +def nodeToString(node, extraFormatter=None): + buf = StringIO() + _nodeToString(node,buf,extraFormatter=extraFormatter) + return buf.getvalue() + + +def _nodeToString(node, buf, extraFormatter=None): + buf.write("{") + if node.label != None: + buf.write("label:") + buf.write(node.label) + + if node.id != None: + buf.write(", id:") + buf.write(str(node.id)) + + if node.properties != None: + buf.write(", properties:{") + prop_list = [] + for k, v in node.properties.items(): + prop_list.append(f"{k}: {str(v)}") + + # Join properties with comma and write to buffer + buf.write(", ".join(prop_list)) + buf.write("}") + + if extraFormatter != None: + extraFormatter(node, buf) + + if node.gtype == TP_VERTEX: + buf.write("}::VERTEX") + if node.gtype == TP_EDGE: + buf.write("}::EDGE") + + +def nodeToJson(node, extraFormatter=None): + buf = StringIO() + _nodeToJson(node, buf, extraFormatter=extraFormatter) + return buf.getvalue() + + +def _nodeToJson(node, buf, extraFormatter=None): + buf.write("{\"gtype\": ") + if node.gtype == TP_VERTEX: + buf.write("\"vertex\", ") + if node.gtype == TP_EDGE: + buf.write("\"edge\", ") + + if node.label != None: + buf.write("\"label\":\"") + buf.write(node.label) + buf.write("\"") + + if node.id != None: + buf.write(", \"id\":") + buf.write(str(node.id)) + + if extraFormatter != None: + extraFormatter(node, buf) + + if node.properties != None: + buf.write(", \"properties\":{") + + prop_list = [] + for k, v in node.properties.items(): + prop_list.append(f"\"{k}\": \"{str(v)}\"") + + # Join properties with comma and write to buffer + buf.write(", ".join(prop_list)) + buf.write("}") + buf.write("}") + \ No newline at end of file diff --git a/packages/ai/src/ai/common/graph/age/_cypher/Cypher.g4 b/packages/ai/src/ai/common/graph/age/_cypher/Cypher.g4 new file mode 100644 index 000000000..549736b5c --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_cypher/Cypher.g4 @@ -0,0 +1,805 @@ +/** + * Copyright (c) 2015-2023 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * 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. + * + * Attribution Notice under the terms of the Apache License 2.0 + * + * This work was created by the collective efforts of the openCypher community. + * Without limiting the terms of Section 6, any Derivative Work that is not + * approved by the public consensus process of the openCypher Implementers Group + * should not be described as “Cypher” (and Cypher® is a registered trademark of + * Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or + * proposals for change that have been documented or implemented should only be + * described as "implementation extensions to Cypher" or as "proposed changes to + * Cypher that are not yet approved by the openCypher community". + */ +grammar Cypher; + +oC_Cypher + : SP? oC_Statement ( SP? ';' )? SP? EOF ; + +oC_Statement + : oC_Query ; + +oC_Query + : oC_RegularQuery + | oC_StandaloneCall + ; + +oC_RegularQuery + : oC_SingleQuery ( SP? oC_Union )* ; + +oC_Union + : ( UNION SP ALL SP? oC_SingleQuery ) + | ( UNION SP? oC_SingleQuery ) + ; + +UNION : ( 'U' | 'u' ) ( 'N' | 'n' ) ( 'I' | 'i' ) ( 'O' | 'o' ) ( 'N' | 'n' ) ; + +ALL : ( 'A' | 'a' ) ( 'L' | 'l' ) ( 'L' | 'l' ) ; + +oC_SingleQuery + : oC_SinglePartQuery + | oC_MultiPartQuery + ; + +oC_SinglePartQuery + : ( ( oC_ReadingClause SP? )* oC_Return ) + | ( ( oC_ReadingClause SP? )* oC_UpdatingClause ( SP? oC_UpdatingClause )* ( SP? oC_Return )? ) + ; + +oC_MultiPartQuery + : ( ( oC_ReadingClause SP? )* ( oC_UpdatingClause SP? )* oC_With SP? )+ oC_SinglePartQuery ; + +oC_UpdatingClause + : oC_Create + | oC_Merge + | oC_Delete + | oC_Set + | oC_Remove + ; + +oC_ReadingClause + : oC_Match + | oC_Unwind + | oC_InQueryCall + ; + +oC_Match + : ( OPTIONAL SP )? MATCH SP? oC_Pattern ( SP? oC_Where )? ; + +OPTIONAL : ( 'O' | 'o' ) ( 'P' | 'p' ) ( 'T' | 't' ) ( 'I' | 'i' ) ( 'O' | 'o' ) ( 'N' | 'n' ) ( 'A' | 'a' ) ( 'L' | 'l' ) ; + +MATCH : ( 'M' | 'm' ) ( 'A' | 'a' ) ( 'T' | 't' ) ( 'C' | 'c' ) ( 'H' | 'h' ) ; + +oC_Unwind + : UNWIND SP? oC_Expression SP AS SP oC_Variable ; + +UNWIND : ( 'U' | 'u' ) ( 'N' | 'n' ) ( 'W' | 'w' ) ( 'I' | 'i' ) ( 'N' | 'n' ) ( 'D' | 'd' ) ; + +AS : ( 'A' | 'a' ) ( 'S' | 's' ) ; + +oC_Merge + : MERGE SP? oC_PatternPart ( SP oC_MergeAction )* ; + +MERGE : ( 'M' | 'm' ) ( 'E' | 'e' ) ( 'R' | 'r' ) ( 'G' | 'g' ) ( 'E' | 'e' ) ; + +oC_MergeAction + : ( ON SP MATCH SP oC_Set ) + | ( ON SP CREATE SP oC_Set ) + ; + +ON : ( 'O' | 'o' ) ( 'N' | 'n' ) ; + +CREATE : ( 'C' | 'c' ) ( 'R' | 'r' ) ( 'E' | 'e' ) ( 'A' | 'a' ) ( 'T' | 't' ) ( 'E' | 'e' ) ; + +oC_Create + : CREATE SP? oC_Pattern ; + +oC_Set + : SET SP? oC_SetItem ( SP? ',' SP? oC_SetItem )* ; + +SET : ( 'S' | 's' ) ( 'E' | 'e' ) ( 'T' | 't' ) ; + +oC_SetItem + : ( oC_PropertyExpression SP? '=' SP? oC_Expression ) + | ( oC_Variable SP? '=' SP? oC_Expression ) + | ( oC_Variable SP? '+=' SP? oC_Expression ) + | ( oC_Variable SP? oC_NodeLabels ) + ; + +oC_Delete + : ( DETACH SP )? DELETE SP? oC_Expression ( SP? ',' SP? oC_Expression )* ; + +DETACH : ( 'D' | 'd' ) ( 'E' | 'e' ) ( 'T' | 't' ) ( 'A' | 'a' ) ( 'C' | 'c' ) ( 'H' | 'h' ) ; + +DELETE : ( 'D' | 'd' ) ( 'E' | 'e' ) ( 'L' | 'l' ) ( 'E' | 'e' ) ( 'T' | 't' ) ( 'E' | 'e' ) ; + +oC_Remove + : REMOVE SP oC_RemoveItem ( SP? ',' SP? oC_RemoveItem )* ; + +REMOVE : ( 'R' | 'r' ) ( 'E' | 'e' ) ( 'M' | 'm' ) ( 'O' | 'o' ) ( 'V' | 'v' ) ( 'E' | 'e' ) ; + +oC_RemoveItem + : ( oC_Variable oC_NodeLabels ) + | oC_PropertyExpression + ; + +oC_InQueryCall + : CALL SP oC_ExplicitProcedureInvocation ( SP? YIELD SP oC_YieldItems )? ; + +CALL : ( 'C' | 'c' ) ( 'A' | 'a' ) ( 'L' | 'l' ) ( 'L' | 'l' ) ; + +YIELD : ( 'Y' | 'y' ) ( 'I' | 'i' ) ( 'E' | 'e' ) ( 'L' | 'l' ) ( 'D' | 'd' ) ; + +oC_StandaloneCall + : CALL SP ( oC_ExplicitProcedureInvocation | oC_ImplicitProcedureInvocation ) ( SP? YIELD SP ( '*' | oC_YieldItems ) )? ; + +oC_YieldItems + : oC_YieldItem ( SP? ',' SP? oC_YieldItem )* ( SP? oC_Where )? ; + +oC_YieldItem + : ( oC_ProcedureResultField SP AS SP )? oC_Variable ; + +oC_With + : WITH oC_ProjectionBody ( SP? oC_Where )? ; + +WITH : ( 'W' | 'w' ) ( 'I' | 'i' ) ( 'T' | 't' ) ( 'H' | 'h' ) ; + +oC_Return + : RETURN oC_ProjectionBody ; + +RETURN : ( 'R' | 'r' ) ( 'E' | 'e' ) ( 'T' | 't' ) ( 'U' | 'u' ) ( 'R' | 'r' ) ( 'N' | 'n' ) ; + +oC_ProjectionBody + : ( SP? DISTINCT )? SP oC_ProjectionItems ( SP oC_Order )? ( SP oC_Skip )? ( SP oC_Limit )? ; + +DISTINCT : ( 'D' | 'd' ) ( 'I' | 'i' ) ( 'S' | 's' ) ( 'T' | 't' ) ( 'I' | 'i' ) ( 'N' | 'n' ) ( 'C' | 'c' ) ( 'T' | 't' ) ; + +oC_ProjectionItems + : ( '*' ( SP? ',' SP? oC_ProjectionItem )* ) + | ( oC_ProjectionItem ( SP? ',' SP? oC_ProjectionItem )* ) + ; + +oC_ProjectionItem + : ( oC_Expression SP AS SP oC_Variable ) + | oC_Expression + ; + +oC_Order + : ORDER SP BY SP oC_SortItem ( ',' SP? oC_SortItem )* ; + +ORDER : ( 'O' | 'o' ) ( 'R' | 'r' ) ( 'D' | 'd' ) ( 'E' | 'e' ) ( 'R' | 'r' ) ; + +BY : ( 'B' | 'b' ) ( 'Y' | 'y' ) ; + +oC_Skip + : L_SKIP SP oC_Expression ; + +L_SKIP : ( 'S' | 's' ) ( 'K' | 'k' ) ( 'I' | 'i' ) ( 'P' | 'p' ) ; + +oC_Limit + : LIMIT SP oC_Expression ; + +LIMIT : ( 'L' | 'l' ) ( 'I' | 'i' ) ( 'M' | 'm' ) ( 'I' | 'i' ) ( 'T' | 't' ) ; + +oC_SortItem + : oC_Expression ( SP? ( ASCENDING | ASC | DESCENDING | DESC ) )? ; + +ASCENDING : ( 'A' | 'a' ) ( 'S' | 's' ) ( 'C' | 'c' ) ( 'E' | 'e' ) ( 'N' | 'n' ) ( 'D' | 'd' ) ( 'I' | 'i' ) ( 'N' | 'n' ) ( 'G' | 'g' ) ; + +ASC : ( 'A' | 'a' ) ( 'S' | 's' ) ( 'C' | 'c' ) ; + +DESCENDING : ( 'D' | 'd' ) ( 'E' | 'e' ) ( 'S' | 's' ) ( 'C' | 'c' ) ( 'E' | 'e' ) ( 'N' | 'n' ) ( 'D' | 'd' ) ( 'I' | 'i' ) ( 'N' | 'n' ) ( 'G' | 'g' ) ; + +DESC : ( 'D' | 'd' ) ( 'E' | 'e' ) ( 'S' | 's' ) ( 'C' | 'c' ) ; + +oC_Where + : WHERE SP oC_Expression ; + +WHERE : ( 'W' | 'w' ) ( 'H' | 'h' ) ( 'E' | 'e' ) ( 'R' | 'r' ) ( 'E' | 'e' ) ; + +oC_Pattern + : oC_PatternPart ( SP? ',' SP? oC_PatternPart )* ; + +oC_PatternPart + : ( oC_Variable SP? '=' SP? oC_AnonymousPatternPart ) + | oC_AnonymousPatternPart + ; + +oC_AnonymousPatternPart + : oC_PatternElement ; + +oC_PatternElement + : ( oC_NodePattern ( SP? oC_PatternElementChain )* ) + | ( '(' oC_PatternElement ')' ) + ; + +oC_RelationshipsPattern + : oC_NodePattern ( SP? oC_PatternElementChain )+ ; + +oC_NodePattern + : '(' SP? ( oC_Variable SP? )? ( oC_NodeLabels SP? )? ( oC_Properties SP? )? ')' ; + +oC_PatternElementChain + : oC_RelationshipPattern SP? oC_NodePattern ; + +oC_RelationshipPattern + : ( oC_LeftArrowHead SP? oC_Dash SP? oC_RelationshipDetail? SP? oC_Dash SP? oC_RightArrowHead ) + | ( oC_LeftArrowHead SP? oC_Dash SP? oC_RelationshipDetail? SP? oC_Dash ) + | ( oC_Dash SP? oC_RelationshipDetail? SP? oC_Dash SP? oC_RightArrowHead ) + | ( oC_Dash SP? oC_RelationshipDetail? SP? oC_Dash ) + ; + +oC_RelationshipDetail + : '[' SP? ( oC_Variable SP? )? ( oC_RelationshipTypes SP? )? oC_RangeLiteral? ( oC_Properties SP? )? ']' ; + +oC_Properties + : oC_MapLiteral + | oC_Parameter + ; + +oC_RelationshipTypes + : ':' SP? oC_RelTypeName ( SP? '|' ':'? SP? oC_RelTypeName )* ; + +oC_NodeLabels + : oC_NodeLabel ( SP? oC_NodeLabel )* ; + +oC_NodeLabel + : ':' SP? oC_LabelName ; + +oC_RangeLiteral + : '*' SP? ( oC_IntegerLiteral SP? )? ( '..' SP? ( oC_IntegerLiteral SP? )? )? ; + +oC_LabelName + : oC_SchemaName ; + +oC_RelTypeName + : oC_SchemaName ; + +oC_PropertyExpression + : oC_Atom ( SP? oC_PropertyLookup )+ ; + +oC_Expression + : oC_OrExpression ; + +oC_OrExpression + : oC_XorExpression ( SP OR SP oC_XorExpression )* ; + +OR : ( 'O' | 'o' ) ( 'R' | 'r' ) ; + +oC_XorExpression + : oC_AndExpression ( SP XOR SP oC_AndExpression )* ; + +XOR : ( 'X' | 'x' ) ( 'O' | 'o' ) ( 'R' | 'r' ) ; + +oC_AndExpression + : oC_NotExpression ( SP AND SP oC_NotExpression )* ; + +AND : ( 'A' | 'a' ) ( 'N' | 'n' ) ( 'D' | 'd' ) ; + +oC_NotExpression + : ( NOT SP? )* oC_ComparisonExpression ; + +NOT : ( 'N' | 'n' ) ( 'O' | 'o' ) ( 'T' | 't' ) ; + +oC_ComparisonExpression + : oC_StringListNullPredicateExpression ( SP? oC_PartialComparisonExpression )* ; + +oC_PartialComparisonExpression + : ( '=' SP? oC_StringListNullPredicateExpression ) + | ( '<>' SP? oC_StringListNullPredicateExpression ) + | ( '<' SP? oC_StringListNullPredicateExpression ) + | ( '>' SP? oC_StringListNullPredicateExpression ) + | ( '<=' SP? oC_StringListNullPredicateExpression ) + | ( '>=' SP? oC_StringListNullPredicateExpression ) + ; + +oC_StringListNullPredicateExpression + : oC_AddOrSubtractExpression ( oC_StringPredicateExpression | oC_ListPredicateExpression | oC_NullPredicateExpression )* ; + +oC_StringPredicateExpression + : ( ( SP STARTS SP WITH ) | ( SP ENDS SP WITH ) | ( SP CONTAINS ) ) SP? oC_AddOrSubtractExpression ; + +STARTS : ( 'S' | 's' ) ( 'T' | 't' ) ( 'A' | 'a' ) ( 'R' | 'r' ) ( 'T' | 't' ) ( 'S' | 's' ) ; + +ENDS : ( 'E' | 'e' ) ( 'N' | 'n' ) ( 'D' | 'd' ) ( 'S' | 's' ) ; + +CONTAINS : ( 'C' | 'c' ) ( 'O' | 'o' ) ( 'N' | 'n' ) ( 'T' | 't' ) ( 'A' | 'a' ) ( 'I' | 'i' ) ( 'N' | 'n' ) ( 'S' | 's' ) ; + +oC_ListPredicateExpression + : SP IN SP? oC_AddOrSubtractExpression ; + +IN : ( 'I' | 'i' ) ( 'N' | 'n' ) ; + +oC_NullPredicateExpression + : ( SP IS SP NULL ) + | ( SP IS SP NOT SP NULL ) + ; + +IS : ( 'I' | 'i' ) ( 'S' | 's' ) ; + +NULL : ( 'N' | 'n' ) ( 'U' | 'u' ) ( 'L' | 'l' ) ( 'L' | 'l' ) ; + +oC_AddOrSubtractExpression + : oC_MultiplyDivideModuloExpression ( ( SP? '+' SP? oC_MultiplyDivideModuloExpression ) | ( SP? '-' SP? oC_MultiplyDivideModuloExpression ) )* ; + +oC_MultiplyDivideModuloExpression + : oC_PowerOfExpression ( ( SP? '*' SP? oC_PowerOfExpression ) | ( SP? '/' SP? oC_PowerOfExpression ) | ( SP? '%' SP? oC_PowerOfExpression ) )* ; + +oC_PowerOfExpression + : oC_UnaryAddOrSubtractExpression ( SP? '^' SP? oC_UnaryAddOrSubtractExpression )* ; + +oC_UnaryAddOrSubtractExpression + : oC_NonArithmeticOperatorExpression + | ( ( '+' | '-' ) SP? oC_NonArithmeticOperatorExpression ) + ; + +oC_NonArithmeticOperatorExpression + : oC_Atom ( ( SP? oC_ListOperatorExpression ) | ( SP? oC_PropertyLookup ) )* ( SP? oC_NodeLabels )? ; + +oC_ListOperatorExpression + : ( '[' oC_Expression ']' ) + | ( '[' oC_Expression? '..' oC_Expression? ']' ) + ; + +oC_PropertyLookup + : '.' SP? ( oC_PropertyKeyName ) ; + +oC_Atom + : oC_Literal + | oC_Parameter + | oC_CaseExpression + | ( COUNT SP? '(' SP? '*' SP? ')' ) + | oC_ListComprehension + | oC_PatternComprehension + | oC_Quantifier + | oC_PatternPredicate + | oC_ParenthesizedExpression + | oC_FunctionInvocation + | oC_ExistentialSubquery + | oC_Variable + ; + +COUNT : ( 'C' | 'c' ) ( 'O' | 'o' ) ( 'U' | 'u' ) ( 'N' | 'n' ) ( 'T' | 't' ) ; + +oC_CaseExpression + : ( ( CASE ( SP? oC_CaseAlternative )+ ) | ( CASE SP? oC_Expression ( SP? oC_CaseAlternative )+ ) ) ( SP? ELSE SP? oC_Expression )? SP? END ; + +CASE : ( 'C' | 'c' ) ( 'A' | 'a' ) ( 'S' | 's' ) ( 'E' | 'e' ) ; + +ELSE : ( 'E' | 'e' ) ( 'L' | 'l' ) ( 'S' | 's' ) ( 'E' | 'e' ) ; + +END : ( 'E' | 'e' ) ( 'N' | 'n' ) ( 'D' | 'd' ) ; + +oC_CaseAlternative + : WHEN SP? oC_Expression SP? THEN SP? oC_Expression ; + +WHEN : ( 'W' | 'w' ) ( 'H' | 'h' ) ( 'E' | 'e' ) ( 'N' | 'n' ) ; + +THEN : ( 'T' | 't' ) ( 'H' | 'h' ) ( 'E' | 'e' ) ( 'N' | 'n' ) ; + +oC_ListComprehension + : '[' SP? oC_FilterExpression ( SP? '|' SP? oC_Expression )? SP? ']' ; + +oC_PatternComprehension + : '[' SP? ( oC_Variable SP? '=' SP? )? oC_RelationshipsPattern SP? ( oC_Where SP? )? '|' SP? oC_Expression SP? ']' ; + +oC_Quantifier + : ( ALL SP? '(' SP? oC_FilterExpression SP? ')' ) + | ( ANY SP? '(' SP? oC_FilterExpression SP? ')' ) + | ( NONE SP? '(' SP? oC_FilterExpression SP? ')' ) + | ( SINGLE SP? '(' SP? oC_FilterExpression SP? ')' ) + ; + +ANY : ( 'A' | 'a' ) ( 'N' | 'n' ) ( 'Y' | 'y' ) ; + +NONE : ( 'N' | 'n' ) ( 'O' | 'o' ) ( 'N' | 'n' ) ( 'E' | 'e' ) ; + +SINGLE : ( 'S' | 's' ) ( 'I' | 'i' ) ( 'N' | 'n' ) ( 'G' | 'g' ) ( 'L' | 'l' ) ( 'E' | 'e' ) ; + +oC_FilterExpression + : oC_IdInColl ( SP? oC_Where )? ; + +oC_PatternPredicate + : oC_RelationshipsPattern ; + +oC_ParenthesizedExpression + : '(' SP? oC_Expression SP? ')' ; + +oC_IdInColl + : oC_Variable SP IN SP oC_Expression ; + +oC_FunctionInvocation + : oC_FunctionName SP? '(' SP? ( DISTINCT SP? )? ( oC_Expression SP? ( ',' SP? oC_Expression SP? )* )? ')' ; + +oC_FunctionName + : oC_Namespace oC_SymbolicName ; + +oC_ExistentialSubquery + : EXISTS SP? '{' SP? ( oC_RegularQuery | ( oC_Pattern ( SP? oC_Where )? ) ) SP? '}' ; + +EXISTS : ( 'E' | 'e' ) ( 'X' | 'x' ) ( 'I' | 'i' ) ( 'S' | 's' ) ( 'T' | 't' ) ( 'S' | 's' ) ; + +oC_ExplicitProcedureInvocation + : oC_ProcedureName SP? '(' SP? ( oC_Expression SP? ( ',' SP? oC_Expression SP? )* )? ')' ; + +oC_ImplicitProcedureInvocation + : oC_ProcedureName ; + +oC_ProcedureResultField + : oC_SymbolicName ; + +oC_ProcedureName + : oC_Namespace oC_SymbolicName ; + +oC_Namespace + : ( oC_SymbolicName '.' )* ; + +oC_Variable + : oC_SymbolicName ; + +oC_Literal + : oC_BooleanLiteral + | NULL + | oC_NumberLiteral + | StringLiteral + | oC_ListLiteral + | oC_MapLiteral + ; + +oC_BooleanLiteral + : TRUE + | FALSE + ; + +TRUE : ( 'T' | 't' ) ( 'R' | 'r' ) ( 'U' | 'u' ) ( 'E' | 'e' ) ; + +FALSE : ( 'F' | 'f' ) ( 'A' | 'a' ) ( 'L' | 'l' ) ( 'S' | 's' ) ( 'E' | 'e' ) ; + +oC_NumberLiteral + : oC_DoubleLiteral + | oC_IntegerLiteral + ; + +oC_IntegerLiteral + : HexInteger + | OctalInteger + | DecimalInteger + ; + +HexInteger + : '0x' ( HexDigit )+ ; + +DecimalInteger + : ZeroDigit + | ( NonZeroDigit ( Digit )* ) + ; + +OctalInteger + : '0o' ( OctDigit )+ ; + +HexLetter + : ( ( 'A' | 'a' ) ) + | ( ( 'B' | 'b' ) ) + | ( ( 'C' | 'c' ) ) + | ( ( 'D' | 'd' ) ) + | ( ( 'E' | 'e' ) ) + | ( ( 'F' | 'f' ) ) + ; + +HexDigit + : Digit + | HexLetter + ; + +Digit + : ZeroDigit + | NonZeroDigit + ; + +NonZeroDigit + : NonZeroOctDigit + | '8' + | '9' + ; + +NonZeroOctDigit + : '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + ; + +OctDigit + : ZeroDigit + | NonZeroOctDigit + ; + +ZeroDigit + : '0' ; + +oC_DoubleLiteral + : ExponentDecimalReal + | RegularDecimalReal + ; + +ExponentDecimalReal + : ( ( Digit )+ | ( ( Digit )+ '.' ( Digit )+ ) | ( '.' ( Digit )+ ) ) ( ( 'E' | 'e' ) ) '-'? ( Digit )+ ; + +RegularDecimalReal + : ( Digit )* '.' ( Digit )+ ; + +StringLiteral + : ( '"' ( StringLiteral_0 | EscapedChar )* '"' ) + | ( '\'' ( StringLiteral_1 | EscapedChar )* '\'' ) + ; + +EscapedChar + : '\\' ( '\\' | '\'' | '"' | ( ( 'B' | 'b' ) ) | ( ( 'F' | 'f' ) ) | ( ( 'N' | 'n' ) ) | ( ( 'R' | 'r' ) ) | ( ( 'T' | 't' ) ) | ( ( ( 'U' | 'u' ) ) ( HexDigit HexDigit HexDigit HexDigit ) ) | ( ( ( 'U' | 'u' ) ) ( HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit ) ) ) ; + +oC_ListLiteral + : '[' SP? ( oC_Expression SP? ( ',' SP? oC_Expression SP? )* )? ']' ; + +oC_MapLiteral + : '{' SP? ( oC_PropertyKeyName SP? ':' SP? oC_Expression SP? ( ',' SP? oC_PropertyKeyName SP? ':' SP? oC_Expression SP? )* )? '}' ; + +oC_PropertyKeyName + : oC_SchemaName ; + +oC_Parameter + : '$' ( oC_SymbolicName | DecimalInteger ) ; + +oC_SchemaName + : oC_SymbolicName + | oC_ReservedWord + ; + +oC_ReservedWord + : ALL + | ASC + | ASCENDING + | BY + | CREATE + | DELETE + | DESC + | DESCENDING + | DETACH + | EXISTS + | LIMIT + | MATCH + | MERGE + | ON + | OPTIONAL + | ORDER + | REMOVE + | RETURN + | SET + | L_SKIP + | WHERE + | WITH + | UNION + | UNWIND + | AND + | AS + | CONTAINS + | DISTINCT + | ENDS + | IN + | IS + | NOT + | OR + | STARTS + | XOR + | FALSE + | TRUE + | NULL + | CONSTRAINT + | DO + | FOR + | REQUIRE + | UNIQUE + | CASE + | WHEN + | THEN + | ELSE + | END + | MANDATORY + | SCALAR + | OF + | ADD + | DROP + ; + +CONSTRAINT : ( 'C' | 'c' ) ( 'O' | 'o' ) ( 'N' | 'n' ) ( 'S' | 's' ) ( 'T' | 't' ) ( 'R' | 'r' ) ( 'A' | 'a' ) ( 'I' | 'i' ) ( 'N' | 'n' ) ( 'T' | 't' ) ; + +DO : ( 'D' | 'd' ) ( 'O' | 'o' ) ; + +FOR : ( 'F' | 'f' ) ( 'O' | 'o' ) ( 'R' | 'r' ) ; + +REQUIRE : ( 'R' | 'r' ) ( 'E' | 'e' ) ( 'Q' | 'q' ) ( 'U' | 'u' ) ( 'I' | 'i' ) ( 'R' | 'r' ) ( 'E' | 'e' ) ; + +UNIQUE : ( 'U' | 'u' ) ( 'N' | 'n' ) ( 'I' | 'i' ) ( 'Q' | 'q' ) ( 'U' | 'u' ) ( 'E' | 'e' ) ; + +MANDATORY : ( 'M' | 'm' ) ( 'A' | 'a' ) ( 'N' | 'n' ) ( 'D' | 'd' ) ( 'A' | 'a' ) ( 'T' | 't' ) ( 'O' | 'o' ) ( 'R' | 'r' ) ( 'Y' | 'y' ) ; + +SCALAR : ( 'S' | 's' ) ( 'C' | 'c' ) ( 'A' | 'a' ) ( 'L' | 'l' ) ( 'A' | 'a' ) ( 'R' | 'r' ) ; + +OF : ( 'O' | 'o' ) ( 'F' | 'f' ) ; + +ADD : ( 'A' | 'a' ) ( 'D' | 'd' ) ( 'D' | 'd' ) ; + +DROP : ( 'D' | 'd' ) ( 'R' | 'r' ) ( 'O' | 'o' ) ( 'P' | 'p' ) ; + +oC_SymbolicName + : UnescapedSymbolicName + | EscapedSymbolicName + | HexLetter + | COUNT + | FILTER + | EXTRACT + | ANY + | NONE + | SINGLE + ; + +FILTER : ( 'F' | 'f' ) ( 'I' | 'i' ) ( 'L' | 'l' ) ( 'T' | 't' ) ( 'E' | 'e' ) ( 'R' | 'r' ) ; + +EXTRACT : ( 'E' | 'e' ) ( 'X' | 'x' ) ( 'T' | 't' ) ( 'R' | 'r' ) ( 'A' | 'a' ) ( 'C' | 'c' ) ( 'T' | 't' ) ; + +UnescapedSymbolicName + : IdentifierStart ( IdentifierPart )* ; + +/** + * Based on the unicode identifier and pattern syntax + * (http://www.unicode.org/reports/tr31/) + * And extended with a few characters. + */ +IdentifierStart + : ID_Start + | Pc + ; + +/** + * Based on the unicode identifier and pattern syntax + * (http://www.unicode.org/reports/tr31/) + * And extended with a few characters. + */ +IdentifierPart + : ID_Continue + | Sc + ; + +/** + * Any character except "`", enclosed within `backticks`. Backticks are escaped with double backticks. + */ +EscapedSymbolicName + : ( '`' ( EscapedSymbolicName_0 )* '`' )+ ; + +SP + : ( WHITESPACE )+ ; + +WHITESPACE + : SPACE + | TAB + | LF + | VT + | FF + | CR + | FS + | GS + | RS + | US + | '\u1680' + | '\u180e' + | '\u2000' + | '\u2001' + | '\u2002' + | '\u2003' + | '\u2004' + | '\u2005' + | '\u2006' + | '\u2008' + | '\u2009' + | '\u200a' + | '\u2028' + | '\u2029' + | '\u205f' + | '\u3000' + | '\u00a0' + | '\u2007' + | '\u202f' + | Comment + ; + +Comment + : ( '/*' ( Comment_1 | ( '*' Comment_2 ) )* '*/' ) + | ( '//' ( Comment_3 )* CR? ( LF | EOF ) ) + ; + +oC_LeftArrowHead + : '<' + | '\u27e8' + | '\u3008' + | '\ufe64' + | '\uff1c' + ; + +oC_RightArrowHead + : '>' + | '\u27e9' + | '\u3009' + | '\ufe65' + | '\uff1e' + ; + +oC_Dash + : '-' + | '\u00ad' + | '\u2010' + | '\u2011' + | '\u2012' + | '\u2013' + | '\u2014' + | '\u2015' + | '\u2212' + | '\ufe58' + | '\ufe63' + | '\uff0d' + ; + +fragment FF : [\f] ; + +fragment EscapedSymbolicName_0 : ~[`] ; + +fragment RS : [\u001E] ; + +fragment ID_Continue : [\p{ID_Continue}] ; + +fragment Comment_1 : ~[*] ; + +fragment StringLiteral_1 : ~['\\] ; + +fragment Comment_3 : ~[\n\r] ; + +fragment Comment_2 : ~[/] ; + +fragment GS : [\u001D] ; + +fragment FS : [\u001C] ; + +fragment CR : [\r] ; + +fragment Sc : [\p{Sc}] ; + +fragment SPACE : [ ] ; + +fragment Pc : [\p{Pc}] ; + +fragment TAB : [\t] ; + +fragment StringLiteral_0 : ~["\\] ; + +fragment LF : [\n] ; + +fragment VT : [\u000B] ; + +fragment US : [\u001F] ; + +fragment ID_Start : [\p{ID_Start}] ; + diff --git a/packages/ai/src/ai/common/graph/age/_cypher/__init__.py b/packages/ai/src/ai/common/graph/age/_cypher/__init__.py new file mode 100644 index 000000000..afab632a7 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_cypher/__init__.py @@ -0,0 +1,6 @@ +# ============================================================================= +# VENDORED — openCypher grammar M23 (Apache-2.0, (c) Neo Technology 2015-2023). +# Source grammar: https://s3.amazonaws.com/artifacts.opencypher.org/M23/Cypher.g4 +# gen/: generated with ANTLR 4.13.2 (-Dlanguage=Python3 -visitor). Committed so +# developers never need Java; see ../README.md for the regeneration step. +# ============================================================================= diff --git a/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherLexer.py b/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherLexer.py new file mode 100644 index 000000000..9f9170af2 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherLexer.py @@ -0,0 +1,832 @@ +# Generated from Cypher.g4 by ANTLR 4.13.2 +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + + +def serializedATN(): + return [ + 4,0,127,993,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5, + 2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2, + 13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7, + 19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2, + 26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31,2,32,7, + 32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,7,38,2, + 39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45,7, + 45,2,46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51,2, + 52,7,52,2,53,7,53,2,54,7,54,2,55,7,55,2,56,7,56,2,57,7,57,2,58,7, + 58,2,59,7,59,2,60,7,60,2,61,7,61,2,62,7,62,2,63,7,63,2,64,7,64,2, + 65,7,65,2,66,7,66,2,67,7,67,2,68,7,68,2,69,7,69,2,70,7,70,2,71,7, + 71,2,72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77,2, + 78,7,78,2,79,7,79,2,80,7,80,2,81,7,81,2,82,7,82,2,83,7,83,2,84,7, + 84,2,85,7,85,2,86,7,86,2,87,7,87,2,88,7,88,2,89,7,89,2,90,7,90,2, + 91,7,91,2,92,7,92,2,93,7,93,2,94,7,94,2,95,7,95,2,96,7,96,2,97,7, + 97,2,98,7,98,2,99,7,99,2,100,7,100,2,101,7,101,2,102,7,102,2,103, + 7,103,2,104,7,104,2,105,7,105,2,106,7,106,2,107,7,107,2,108,7,108, + 2,109,7,109,2,110,7,110,2,111,7,111,2,112,7,112,2,113,7,113,2,114, + 7,114,2,115,7,115,2,116,7,116,2,117,7,117,2,118,7,118,2,119,7,119, + 2,120,7,120,2,121,7,121,2,122,7,122,2,123,7,123,2,124,7,124,2,125, + 7,125,2,126,7,126,2,127,7,127,2,128,7,128,2,129,7,129,2,130,7,130, + 2,131,7,131,2,132,7,132,2,133,7,133,2,134,7,134,2,135,7,135,2,136, + 7,136,2,137,7,137,2,138,7,138,2,139,7,139,2,140,7,140,2,141,7,141, + 2,142,7,142,2,143,7,143,2,144,7,144,2,145,7,145,2,146,7,146,1,0, + 1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,3,1,4,1,4,1,5,1,5,1,6,1,6,1,7,1,7, + 1,8,1,8,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,12,1,12,1,13,1,13, + 1,14,1,14,1,15,1,15,1,15,1,16,1,16,1,16,1,17,1,17,1,18,1,18,1,19, + 1,19,1,20,1,20,1,21,1,21,1,22,1,22,1,23,1,23,1,24,1,24,1,25,1,25, + 1,26,1,26,1,27,1,27,1,28,1,28,1,29,1,29,1,30,1,30,1,31,1,31,1,32, + 1,32,1,33,1,33,1,34,1,34,1,35,1,35,1,36,1,36,1,37,1,37,1,38,1,38, + 1,39,1,39,1,40,1,40,1,41,1,41,1,42,1,42,1,43,1,43,1,44,1,44,1,45, + 1,45,1,45,1,45,1,45,1,45,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1,47, + 1,47,1,47,1,47,1,47,1,47,1,48,1,48,1,48,1,48,1,48,1,48,1,49,1,49, + 1,49,1,49,1,49,1,49,1,49,1,50,1,50,1,50,1,51,1,51,1,51,1,51,1,51, + 1,51,1,52,1,52,1,52,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,54,1,54, + 1,54,1,54,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,56,1,56,1,56,1,56, + 1,56,1,56,1,56,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,58,1,58,1,58, + 1,58,1,58,1,59,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60,1,60,1,60, + 1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,62,1,62,1,62,1,62,1,62,1,62, + 1,62,1,62,1,62,1,63,1,63,1,63,1,63,1,63,1,63,1,64,1,64,1,64,1,65, + 1,65,1,65,1,65,1,65,1,66,1,66,1,66,1,66,1,66,1,66,1,67,1,67,1,67, + 1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,68,1,68,1,68,1,68,1,69,1,69, + 1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,70,1,70,1,70,1,70, + 1,70,1,71,1,71,1,71,1,71,1,71,1,71,1,72,1,72,1,72,1,73,1,73,1,73, + 1,73,1,74,1,74,1,74,1,74,1,75,1,75,1,75,1,75,1,76,1,76,1,76,1,76, + 1,76,1,76,1,76,1,77,1,77,1,77,1,77,1,77,1,78,1,78,1,78,1,78,1,78, + 1,78,1,78,1,78,1,78,1,79,1,79,1,79,1,80,1,80,1,80,1,81,1,81,1,81, + 1,81,1,81,1,82,1,82,1,82,1,82,1,82,1,82,1,83,1,83,1,83,1,83,1,83, + 1,84,1,84,1,84,1,84,1,84,1,85,1,85,1,85,1,85,1,86,1,86,1,86,1,86, + 1,86,1,87,1,87,1,87,1,87,1,87,1,88,1,88,1,88,1,88,1,89,1,89,1,89, + 1,89,1,89,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,91,1,91,1,91,1,91, + 1,91,1,91,1,91,1,92,1,92,1,92,1,92,1,92,1,93,1,93,1,93,1,93,1,93, + 1,93,1,94,1,94,1,94,1,94,4,94,670,8,94,11,94,12,94,671,1,95,1,95, + 1,95,5,95,677,8,95,10,95,12,95,680,9,95,3,95,682,8,95,1,96,1,96, + 1,96,1,96,4,96,688,8,96,11,96,12,96,689,1,97,3,97,693,8,97,1,98, + 1,98,3,98,697,8,98,1,99,1,99,3,99,701,8,99,1,100,1,100,3,100,705, + 8,100,1,101,1,101,1,102,1,102,3,102,711,8,102,1,103,1,103,1,104, + 4,104,716,8,104,11,104,12,104,717,1,104,4,104,721,8,104,11,104,12, + 104,722,1,104,1,104,4,104,727,8,104,11,104,12,104,728,1,104,1,104, + 4,104,733,8,104,11,104,12,104,734,3,104,737,8,104,1,104,1,104,3, + 104,741,8,104,1,104,4,104,744,8,104,11,104,12,104,745,1,105,5,105, + 749,8,105,10,105,12,105,752,9,105,1,105,1,105,4,105,756,8,105,11, + 105,12,105,757,1,106,1,106,1,106,5,106,763,8,106,10,106,12,106,766, + 9,106,1,106,1,106,1,106,1,106,5,106,772,8,106,10,106,12,106,775, + 9,106,1,106,3,106,778,8,106,1,107,1,107,1,107,1,107,1,107,1,107, + 1,107,1,107,1,107,1,107,1,107,1,107,1,107,1,107,1,107,1,107,1,107, + 1,107,3,107,798,8,107,1,108,1,108,1,108,1,108,1,108,1,108,1,108, + 1,108,1,108,1,108,1,108,1,109,1,109,1,109,1,110,1,110,1,110,1,110, + 1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,112,1,112,1,112, + 1,112,1,112,1,112,1,112,1,113,1,113,1,113,1,113,1,113,1,113,1,113, + 1,113,1,113,1,113,1,114,1,114,1,114,1,114,1,114,1,114,1,114,1,115, + 1,115,1,115,1,116,1,116,1,116,1,116,1,117,1,117,1,117,1,117,1,117, + 1,118,1,118,1,118,1,118,1,118,1,118,1,118,1,119,1,119,1,119,1,119, + 1,119,1,119,1,119,1,119,1,120,1,120,5,120,879,8,120,10,120,12,120, + 882,9,120,1,121,1,121,3,121,886,8,121,1,122,1,122,3,122,890,8,122, + 1,123,1,123,5,123,894,8,123,10,123,12,123,897,9,123,1,123,4,123, + 900,8,123,11,123,12,123,901,1,124,4,124,905,8,124,11,124,12,124, + 906,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125, + 1,125,1,125,3,125,921,8,125,1,126,1,126,1,126,1,126,1,126,1,126, + 5,126,929,8,126,10,126,12,126,932,9,126,1,126,1,126,1,126,1,126, + 1,126,1,126,5,126,940,8,126,10,126,12,126,943,9,126,1,126,3,126, + 946,8,126,1,126,1,126,3,126,950,8,126,3,126,952,8,126,1,127,1,127, + 1,128,1,128,1,129,1,129,1,130,1,130,1,131,1,131,1,132,1,132,1,133, + 1,133,1,134,1,134,1,135,1,135,1,136,1,136,1,137,1,137,1,138,1,138, + 1,139,1,139,1,140,1,140,1,141,1,141,1,142,1,142,1,143,1,143,1,144, + 1,144,1,145,1,145,1,146,1,146,0,0,147,1,1,3,2,5,3,7,4,9,5,11,6,13, + 7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18, + 37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,27,55,28,57,29, + 59,30,61,31,63,32,65,33,67,34,69,35,71,36,73,37,75,38,77,39,79,40, + 81,41,83,42,85,43,87,44,89,45,91,46,93,47,95,48,97,49,99,50,101, + 51,103,52,105,53,107,54,109,55,111,56,113,57,115,58,117,59,119,60, + 121,61,123,62,125,63,127,64,129,65,131,66,133,67,135,68,137,69,139, + 70,141,71,143,72,145,73,147,74,149,75,151,76,153,77,155,78,157,79, + 159,80,161,81,163,82,165,83,167,84,169,85,171,86,173,87,175,88,177, + 89,179,90,181,91,183,92,185,93,187,94,189,95,191,96,193,97,195,98, + 197,99,199,100,201,101,203,102,205,103,207,104,209,105,211,106,213, + 107,215,108,217,109,219,110,221,111,223,112,225,113,227,114,229, + 115,231,116,233,117,235,118,237,119,239,120,241,121,243,122,245, + 123,247,124,249,125,251,126,253,127,255,0,257,0,259,0,261,0,263, + 0,265,0,267,0,269,0,271,0,273,0,275,0,277,0,279,0,281,0,283,0,285, + 0,287,0,289,0,291,0,293,0,1,0,47,2,0,85,85,117,117,2,0,78,78,110, + 110,2,0,73,73,105,105,2,0,79,79,111,111,2,0,65,65,97,97,2,0,76,76, + 108,108,2,0,80,80,112,112,2,0,84,84,116,116,2,0,77,77,109,109,2, + 0,67,67,99,99,2,0,72,72,104,104,2,0,87,87,119,119,2,0,68,68,100, + 100,2,0,83,83,115,115,2,0,69,69,101,101,2,0,82,82,114,114,2,0,71, + 71,103,103,2,0,86,86,118,118,2,0,89,89,121,121,2,0,66,66,98,98,2, + 0,75,75,107,107,2,0,88,88,120,120,2,0,70,70,102,102,2,0,65,70,97, + 102,13,0,34,34,39,39,66,66,70,70,78,78,82,82,84,84,92,92,98,98,102, + 102,110,110,114,114,116,116,2,0,81,81,113,113,8,0,160,160,5760,5760, + 6158,6158,8192,8202,8232,8233,8239,8239,8287,8287,12288,12288,1, + 0,12,12,1,0,96,96,1,0,30,30,768,0,48,57,65,90,95,95,97,122,170,170, + 181,181,183,183,186,186,192,214,216,246,248,705,710,721,736,740, + 748,748,750,750,768,884,886,887,890,893,895,895,902,906,908,908, + 910,929,931,1013,1015,1153,1155,1159,1162,1327,1329,1366,1369,1369, + 1376,1416,1425,1469,1471,1471,1473,1474,1476,1477,1479,1479,1488, + 1514,1519,1522,1552,1562,1568,1641,1646,1747,1749,1756,1759,1768, + 1770,1788,1791,1791,1808,1866,1869,1969,1984,2037,2042,2042,2045, + 2045,2048,2093,2112,2139,2144,2154,2160,2183,2185,2190,2200,2273, + 2275,2403,2406,2415,2417,2435,2437,2444,2447,2448,2451,2472,2474, + 2480,2482,2482,2486,2489,2492,2500,2503,2504,2507,2510,2519,2519, + 2524,2525,2527,2531,2534,2545,2556,2556,2558,2558,2561,2563,2565, + 2570,2575,2576,2579,2600,2602,2608,2610,2611,2613,2614,2616,2617, + 2620,2620,2622,2626,2631,2632,2635,2637,2641,2641,2649,2652,2654, + 2654,2662,2677,2689,2691,2693,2701,2703,2705,2707,2728,2730,2736, + 2738,2739,2741,2745,2748,2757,2759,2761,2763,2765,2768,2768,2784, + 2787,2790,2799,2809,2815,2817,2819,2821,2828,2831,2832,2835,2856, + 2858,2864,2866,2867,2869,2873,2876,2884,2887,2888,2891,2893,2901, + 2903,2908,2909,2911,2915,2918,2927,2929,2929,2946,2947,2949,2954, + 2958,2960,2962,2965,2969,2970,2972,2972,2974,2975,2979,2980,2984, + 2986,2990,3001,3006,3010,3014,3016,3018,3021,3024,3024,3031,3031, + 3046,3055,3072,3084,3086,3088,3090,3112,3114,3129,3132,3140,3142, + 3144,3146,3149,3157,3158,3160,3162,3165,3165,3168,3171,3174,3183, + 3200,3203,3205,3212,3214,3216,3218,3240,3242,3251,3253,3257,3260, + 3268,3270,3272,3274,3277,3285,3286,3293,3294,3296,3299,3302,3311, + 3313,3315,3328,3340,3342,3344,3346,3396,3398,3400,3402,3406,3412, + 3415,3423,3427,3430,3439,3450,3455,3457,3459,3461,3478,3482,3505, + 3507,3515,3517,3517,3520,3526,3530,3530,3535,3540,3542,3542,3544, + 3551,3558,3567,3570,3571,3585,3642,3648,3662,3664,3673,3713,3714, + 3716,3716,3718,3722,3724,3747,3749,3749,3751,3773,3776,3780,3782, + 3782,3784,3790,3792,3801,3804,3807,3840,3840,3864,3865,3872,3881, + 3893,3893,3895,3895,3897,3897,3902,3911,3913,3948,3953,3972,3974, + 3991,3993,4028,4038,4038,4096,4169,4176,4253,4256,4293,4295,4295, + 4301,4301,4304,4346,4348,4680,4682,4685,4688,4694,4696,4696,4698, + 4701,4704,4744,4746,4749,4752,4784,4786,4789,4792,4798,4800,4800, + 4802,4805,4808,4822,4824,4880,4882,4885,4888,4954,4957,4959,4969, + 4977,4992,5007,5024,5109,5112,5117,5121,5740,5743,5759,5761,5786, + 5792,5866,5870,5880,5888,5909,5919,5940,5952,5971,5984,5996,5998, + 6000,6002,6003,6016,6099,6103,6103,6108,6109,6112,6121,6155,6157, + 6159,6169,6176,6264,6272,6314,6320,6389,6400,6430,6432,6443,6448, + 6459,6470,6509,6512,6516,6528,6571,6576,6601,6608,6618,6656,6683, + 6688,6750,6752,6780,6783,6793,6800,6809,6823,6823,6832,6845,6847, + 6862,6912,6988,6992,7001,7019,7027,7040,7155,7168,7223,7232,7241, + 7245,7293,7296,7304,7312,7354,7357,7359,7376,7378,7380,7418,7424, + 7957,7960,7965,7968,8005,8008,8013,8016,8023,8025,8025,8027,8027, + 8029,8029,8031,8061,8064,8116,8118,8124,8126,8126,8130,8132,8134, + 8140,8144,8147,8150,8155,8160,8172,8178,8180,8182,8188,8255,8256, + 8276,8276,8305,8305,8319,8319,8336,8348,8400,8412,8417,8417,8421, + 8432,8450,8450,8455,8455,8458,8467,8469,8469,8472,8477,8484,8484, + 8486,8486,8488,8488,8490,8505,8508,8511,8517,8521,8526,8526,8544, + 8584,11264,11492,11499,11507,11520,11557,11559,11559,11565,11565, + 11568,11623,11631,11631,11647,11670,11680,11686,11688,11694,11696, + 11702,11704,11710,11712,11718,11720,11726,11728,11734,11736,11742, + 11744,11775,12293,12295,12321,12335,12337,12341,12344,12348,12353, + 12438,12441,12447,12449,12538,12540,12543,12549,12591,12593,12686, + 12704,12735,12784,12799,13312,19903,19968,42124,42192,42237,42240, + 42508,42512,42539,42560,42607,42612,42621,42623,42737,42775,42783, + 42786,42888,42891,42954,42960,42961,42963,42963,42965,42969,42994, + 43047,43052,43052,43072,43123,43136,43205,43216,43225,43232,43255, + 43259,43259,43261,43309,43312,43347,43360,43388,43392,43456,43471, + 43481,43488,43518,43520,43574,43584,43597,43600,43609,43616,43638, + 43642,43714,43739,43741,43744,43759,43762,43766,43777,43782,43785, + 43790,43793,43798,43808,43814,43816,43822,43824,43866,43868,43881, + 43888,44010,44012,44013,44016,44025,44032,55203,55216,55238,55243, + 55291,63744,64109,64112,64217,64256,64262,64275,64279,64285,64296, + 64298,64310,64312,64316,64318,64318,64320,64321,64323,64324,64326, + 64433,64467,64829,64848,64911,64914,64967,65008,65019,65024,65039, + 65056,65071,65075,65076,65101,65103,65136,65140,65142,65276,65296, + 65305,65313,65338,65343,65343,65345,65370,65382,65470,65474,65479, + 65482,65487,65490,65495,65498,65500,65536,65547,65549,65574,65576, + 65594,65596,65597,65599,65613,65616,65629,65664,65786,65856,65908, + 66045,66045,66176,66204,66208,66256,66272,66272,66304,66335,66349, + 66378,66384,66426,66432,66461,66464,66499,66504,66511,66513,66517, + 66560,66717,66720,66729,66736,66771,66776,66811,66816,66855,66864, + 66915,66928,66938,66940,66954,66956,66962,66964,66965,66967,66977, + 66979,66993,66995,67001,67003,67004,67072,67382,67392,67413,67424, + 67431,67456,67461,67463,67504,67506,67514,67584,67589,67592,67592, + 67594,67637,67639,67640,67644,67644,67647,67669,67680,67702,67712, + 67742,67808,67826,67828,67829,67840,67861,67872,67897,67968,68023, + 68030,68031,68096,68099,68101,68102,68108,68115,68117,68119,68121, + 68149,68152,68154,68159,68159,68192,68220,68224,68252,68288,68295, + 68297,68326,68352,68405,68416,68437,68448,68466,68480,68497,68608, + 68680,68736,68786,68800,68850,68864,68903,68912,68921,69248,69289, + 69291,69292,69296,69297,69373,69404,69415,69415,69424,69456,69488, + 69509,69552,69572,69600,69622,69632,69702,69734,69749,69759,69818, + 69826,69826,69840,69864,69872,69881,69888,69940,69942,69951,69956, + 69959,69968,70003,70006,70006,70016,70084,70089,70092,70094,70106, + 70108,70108,70144,70161,70163,70199,70206,70209,70272,70278,70280, + 70280,70282,70285,70287,70301,70303,70312,70320,70378,70384,70393, + 70400,70403,70405,70412,70415,70416,70419,70440,70442,70448,70450, + 70451,70453,70457,70459,70468,70471,70472,70475,70477,70480,70480, + 70487,70487,70493,70499,70502,70508,70512,70516,70656,70730,70736, + 70745,70750,70753,70784,70853,70855,70855,70864,70873,71040,71093, + 71096,71104,71128,71133,71168,71232,71236,71236,71248,71257,71296, + 71352,71360,71369,71424,71450,71453,71467,71472,71481,71488,71494, + 71680,71738,71840,71913,71935,71942,71945,71945,71948,71955,71957, + 71958,71960,71989,71991,71992,71995,72003,72016,72025,72096,72103, + 72106,72151,72154,72161,72163,72164,72192,72254,72263,72263,72272, + 72345,72349,72349,72368,72440,72704,72712,72714,72758,72760,72768, + 72784,72793,72818,72847,72850,72871,72873,72886,72960,72966,72968, + 72969,72971,73014,73018,73018,73020,73021,73023,73031,73040,73049, + 73056,73061,73063,73064,73066,73102,73104,73105,73107,73112,73120, + 73129,73440,73462,73472,73488,73490,73530,73534,73538,73552,73561, + 73648,73648,73728,74649,74752,74862,74880,75075,77712,77808,77824, + 78895,78912,78933,82944,83526,92160,92728,92736,92766,92768,92777, + 92784,92862,92864,92873,92880,92909,92912,92916,92928,92982,92992, + 92995,93008,93017,93027,93047,93053,93071,93760,93823,93952,94026, + 94031,94087,94095,94111,94176,94177,94179,94180,94192,94193,94208, + 100343,100352,101589,101632,101640,110576,110579,110581,110587,110589, + 110590,110592,110882,110898,110898,110928,110930,110933,110933,110948, + 110951,110960,111355,113664,113770,113776,113788,113792,113800,113808, + 113817,113821,113822,118528,118573,118576,118598,119141,119145,119149, + 119154,119163,119170,119173,119179,119210,119213,119362,119364,119808, + 119892,119894,119964,119966,119967,119970,119970,119973,119974,119977, + 119980,119982,119993,119995,119995,119997,120003,120005,120069,120071, + 120074,120077,120084,120086,120092,120094,120121,120123,120126,120128, + 120132,120134,120134,120138,120144,120146,120485,120488,120512,120514, + 120538,120540,120570,120572,120596,120598,120628,120630,120654,120656, + 120686,120688,120712,120714,120744,120746,120770,120772,120779,120782, + 120831,121344,121398,121403,121452,121461,121461,121476,121476,121499, + 121503,121505,121519,122624,122654,122661,122666,122880,122886,122888, + 122904,122907,122913,122915,122916,122918,122922,122928,122989,123023, + 123023,123136,123180,123184,123197,123200,123209,123214,123214,123536, + 123566,123584,123641,124112,124153,124896,124902,124904,124907,124909, + 124910,124912,124926,124928,125124,125136,125142,125184,125259,125264, + 125273,126464,126467,126469,126495,126497,126498,126500,126500,126503, + 126503,126505,126514,126516,126519,126521,126521,126523,126523,126530, + 126530,126535,126535,126537,126537,126539,126539,126541,126543,126545, + 126546,126548,126548,126551,126551,126553,126553,126555,126555,126557, + 126557,126559,126559,126561,126562,126564,126564,126567,126570,126572, + 126578,126580,126583,126585,126588,126590,126590,126592,126601,126603, + 126619,126625,126627,126629,126633,126635,126651,130032,130041,131072, + 173791,173824,177977,177984,178205,178208,183969,183984,191456,194560, + 195101,196608,201546,201552,205743,917760,917999,1,0,42,42,2,0,39, + 39,92,92,2,0,10,10,13,13,1,0,47,47,1,0,29,29,1,0,28,28,1,0,13,13, + 21,0,36,36,162,165,1423,1423,1547,1547,2046,2047,2546,2547,2555, + 2555,2801,2801,3065,3065,3647,3647,6107,6107,8352,8384,43064,43064, + 65020,65020,65129,65129,65284,65284,65504,65505,65509,65510,73693, + 73696,123647,123647,126128,126128,1,0,32,32,6,0,95,95,8255,8256, + 8276,8276,65075,65076,65101,65103,65343,65343,1,0,9,9,2,0,34,34, + 92,92,1,0,10,10,1,0,11,11,1,0,31,31,659,0,65,90,97,122,170,170,181, + 181,186,186,192,214,216,246,248,705,710,721,736,740,748,748,750, + 750,880,884,886,887,890,893,895,895,902,902,904,906,908,908,910, + 929,931,1013,1015,1153,1162,1327,1329,1366,1369,1369,1376,1416,1488, + 1514,1519,1522,1568,1610,1646,1647,1649,1747,1749,1749,1765,1766, + 1774,1775,1786,1788,1791,1791,1808,1808,1810,1839,1869,1957,1969, + 1969,1994,2026,2036,2037,2042,2042,2048,2069,2074,2074,2084,2084, + 2088,2088,2112,2136,2144,2154,2160,2183,2185,2190,2208,2249,2308, + 2361,2365,2365,2384,2384,2392,2401,2417,2432,2437,2444,2447,2448, + 2451,2472,2474,2480,2482,2482,2486,2489,2493,2493,2510,2510,2524, + 2525,2527,2529,2544,2545,2556,2556,2565,2570,2575,2576,2579,2600, + 2602,2608,2610,2611,2613,2614,2616,2617,2649,2652,2654,2654,2674, + 2676,2693,2701,2703,2705,2707,2728,2730,2736,2738,2739,2741,2745, + 2749,2749,2768,2768,2784,2785,2809,2809,2821,2828,2831,2832,2835, + 2856,2858,2864,2866,2867,2869,2873,2877,2877,2908,2909,2911,2913, + 2929,2929,2947,2947,2949,2954,2958,2960,2962,2965,2969,2970,2972, + 2972,2974,2975,2979,2980,2984,2986,2990,3001,3024,3024,3077,3084, + 3086,3088,3090,3112,3114,3129,3133,3133,3160,3162,3165,3165,3168, + 3169,3200,3200,3205,3212,3214,3216,3218,3240,3242,3251,3253,3257, + 3261,3261,3293,3294,3296,3297,3313,3314,3332,3340,3342,3344,3346, + 3386,3389,3389,3406,3406,3412,3414,3423,3425,3450,3455,3461,3478, + 3482,3505,3507,3515,3517,3517,3520,3526,3585,3632,3634,3635,3648, + 3654,3713,3714,3716,3716,3718,3722,3724,3747,3749,3749,3751,3760, + 3762,3763,3773,3773,3776,3780,3782,3782,3804,3807,3840,3840,3904, + 3911,3913,3948,3976,3980,4096,4138,4159,4159,4176,4181,4186,4189, + 4193,4193,4197,4198,4206,4208,4213,4225,4238,4238,4256,4293,4295, + 4295,4301,4301,4304,4346,4348,4680,4682,4685,4688,4694,4696,4696, + 4698,4701,4704,4744,4746,4749,4752,4784,4786,4789,4792,4798,4800, + 4800,4802,4805,4808,4822,4824,4880,4882,4885,4888,4954,4992,5007, + 5024,5109,5112,5117,5121,5740,5743,5759,5761,5786,5792,5866,5870, + 5880,5888,5905,5919,5937,5952,5969,5984,5996,5998,6000,6016,6067, + 6103,6103,6108,6108,6176,6264,6272,6312,6314,6314,6320,6389,6400, + 6430,6480,6509,6512,6516,6528,6571,6576,6601,6656,6678,6688,6740, + 6823,6823,6917,6963,6981,6988,7043,7072,7086,7087,7098,7141,7168, + 7203,7245,7247,7258,7293,7296,7304,7312,7354,7357,7359,7401,7404, + 7406,7411,7413,7414,7418,7418,7424,7615,7680,7957,7960,7965,7968, + 8005,8008,8013,8016,8023,8025,8025,8027,8027,8029,8029,8031,8061, + 8064,8116,8118,8124,8126,8126,8130,8132,8134,8140,8144,8147,8150, + 8155,8160,8172,8178,8180,8182,8188,8305,8305,8319,8319,8336,8348, + 8450,8450,8455,8455,8458,8467,8469,8469,8472,8477,8484,8484,8486, + 8486,8488,8488,8490,8505,8508,8511,8517,8521,8526,8526,8544,8584, + 11264,11492,11499,11502,11506,11507,11520,11557,11559,11559,11565, + 11565,11568,11623,11631,11631,11648,11670,11680,11686,11688,11694, + 11696,11702,11704,11710,11712,11718,11720,11726,11728,11734,11736, + 11742,12293,12295,12321,12329,12337,12341,12344,12348,12353,12438, + 12443,12447,12449,12538,12540,12543,12549,12591,12593,12686,12704, + 12735,12784,12799,13312,19903,19968,42124,42192,42237,42240,42508, + 42512,42527,42538,42539,42560,42606,42623,42653,42656,42735,42775, + 42783,42786,42888,42891,42954,42960,42961,42963,42963,42965,42969, + 42994,43009,43011,43013,43015,43018,43020,43042,43072,43123,43138, + 43187,43250,43255,43259,43259,43261,43262,43274,43301,43312,43334, + 43360,43388,43396,43442,43471,43471,43488,43492,43494,43503,43514, + 43518,43520,43560,43584,43586,43588,43595,43616,43638,43642,43642, + 43646,43695,43697,43697,43701,43702,43705,43709,43712,43712,43714, + 43714,43739,43741,43744,43754,43762,43764,43777,43782,43785,43790, + 43793,43798,43808,43814,43816,43822,43824,43866,43868,43881,43888, + 44002,44032,55203,55216,55238,55243,55291,63744,64109,64112,64217, + 64256,64262,64275,64279,64285,64285,64287,64296,64298,64310,64312, + 64316,64318,64318,64320,64321,64323,64324,64326,64433,64467,64829, + 64848,64911,64914,64967,65008,65019,65136,65140,65142,65276,65313, + 65338,65345,65370,65382,65470,65474,65479,65482,65487,65490,65495, + 65498,65500,65536,65547,65549,65574,65576,65594,65596,65597,65599, + 65613,65616,65629,65664,65786,65856,65908,66176,66204,66208,66256, + 66304,66335,66349,66378,66384,66421,66432,66461,66464,66499,66504, + 66511,66513,66517,66560,66717,66736,66771,66776,66811,66816,66855, + 66864,66915,66928,66938,66940,66954,66956,66962,66964,66965,66967, + 66977,66979,66993,66995,67001,67003,67004,67072,67382,67392,67413, + 67424,67431,67456,67461,67463,67504,67506,67514,67584,67589,67592, + 67592,67594,67637,67639,67640,67644,67644,67647,67669,67680,67702, + 67712,67742,67808,67826,67828,67829,67840,67861,67872,67897,67968, + 68023,68030,68031,68096,68096,68112,68115,68117,68119,68121,68149, + 68192,68220,68224,68252,68288,68295,68297,68324,68352,68405,68416, + 68437,68448,68466,68480,68497,68608,68680,68736,68786,68800,68850, + 68864,68899,69248,69289,69296,69297,69376,69404,69415,69415,69424, + 69445,69488,69505,69552,69572,69600,69622,69635,69687,69745,69746, + 69749,69749,69763,69807,69840,69864,69891,69926,69956,69956,69959, + 69959,69968,70002,70006,70006,70019,70066,70081,70084,70106,70106, + 70108,70108,70144,70161,70163,70187,70207,70208,70272,70278,70280, + 70280,70282,70285,70287,70301,70303,70312,70320,70366,70405,70412, + 70415,70416,70419,70440,70442,70448,70450,70451,70453,70457,70461, + 70461,70480,70480,70493,70497,70656,70708,70727,70730,70751,70753, + 70784,70831,70852,70853,70855,70855,71040,71086,71128,71131,71168, + 71215,71236,71236,71296,71338,71352,71352,71424,71450,71488,71494, + 71680,71723,71840,71903,71935,71942,71945,71945,71948,71955,71957, + 71958,71960,71983,71999,71999,72001,72001,72096,72103,72106,72144, + 72161,72161,72163,72163,72192,72192,72203,72242,72250,72250,72272, + 72272,72284,72329,72349,72349,72368,72440,72704,72712,72714,72750, + 72768,72768,72818,72847,72960,72966,72968,72969,72971,73008,73030, + 73030,73056,73061,73063,73064,73066,73097,73112,73112,73440,73458, + 73474,73474,73476,73488,73490,73523,73648,73648,73728,74649,74752, + 74862,74880,75075,77712,77808,77824,78895,78913,78918,82944,83526, + 92160,92728,92736,92766,92784,92862,92880,92909,92928,92975,92992, + 92995,93027,93047,93053,93071,93760,93823,93952,94026,94032,94032, + 94099,94111,94176,94177,94179,94179,94208,100343,100352,101589,101632, + 101640,110576,110579,110581,110587,110589,110590,110592,110882,110898, + 110898,110928,110930,110933,110933,110948,110951,110960,111355,113664, + 113770,113776,113788,113792,113800,113808,113817,119808,119892,119894, + 119964,119966,119967,119970,119970,119973,119974,119977,119980,119982, + 119993,119995,119995,119997,120003,120005,120069,120071,120074,120077, + 120084,120086,120092,120094,120121,120123,120126,120128,120132,120134, + 120134,120138,120144,120146,120485,120488,120512,120514,120538,120540, + 120570,120572,120596,120598,120628,120630,120654,120656,120686,120688, + 120712,120714,120744,120746,120770,120772,120779,122624,122654,122661, + 122666,122928,122989,123136,123180,123191,123197,123214,123214,123536, + 123565,123584,123627,124112,124139,124896,124902,124904,124907,124909, + 124910,124912,124926,124928,125124,125184,125251,125259,125259,126464, + 126467,126469,126495,126497,126498,126500,126500,126503,126503,126505, + 126514,126516,126519,126521,126521,126523,126523,126530,126530,126535, + 126535,126537,126537,126539,126539,126541,126543,126545,126546,126548, + 126548,126551,126551,126553,126553,126555,126555,126557,126557,126559, + 126559,126561,126562,126564,126564,126567,126570,126572,126578,126580, + 126583,126585,126588,126590,126590,126592,126601,126603,126619,126625, + 126627,126629,126633,126635,126651,131072,173791,173824,177977,177984, + 178205,178208,183969,183984,191456,194560,195101,196608,201546,201552, + 205743,1020,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9, + 1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19, + 1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29, + 1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39, + 1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49, + 1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0,0,59, + 1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,65,1,0,0,0,0,67,1,0,0,0,0,69, + 1,0,0,0,0,71,1,0,0,0,0,73,1,0,0,0,0,75,1,0,0,0,0,77,1,0,0,0,0,79, + 1,0,0,0,0,81,1,0,0,0,0,83,1,0,0,0,0,85,1,0,0,0,0,87,1,0,0,0,0,89, + 1,0,0,0,0,91,1,0,0,0,0,93,1,0,0,0,0,95,1,0,0,0,0,97,1,0,0,0,0,99, + 1,0,0,0,0,101,1,0,0,0,0,103,1,0,0,0,0,105,1,0,0,0,0,107,1,0,0,0, + 0,109,1,0,0,0,0,111,1,0,0,0,0,113,1,0,0,0,0,115,1,0,0,0,0,117,1, + 0,0,0,0,119,1,0,0,0,0,121,1,0,0,0,0,123,1,0,0,0,0,125,1,0,0,0,0, + 127,1,0,0,0,0,129,1,0,0,0,0,131,1,0,0,0,0,133,1,0,0,0,0,135,1,0, + 0,0,0,137,1,0,0,0,0,139,1,0,0,0,0,141,1,0,0,0,0,143,1,0,0,0,0,145, + 1,0,0,0,0,147,1,0,0,0,0,149,1,0,0,0,0,151,1,0,0,0,0,153,1,0,0,0, + 0,155,1,0,0,0,0,157,1,0,0,0,0,159,1,0,0,0,0,161,1,0,0,0,0,163,1, + 0,0,0,0,165,1,0,0,0,0,167,1,0,0,0,0,169,1,0,0,0,0,171,1,0,0,0,0, + 173,1,0,0,0,0,175,1,0,0,0,0,177,1,0,0,0,0,179,1,0,0,0,0,181,1,0, + 0,0,0,183,1,0,0,0,0,185,1,0,0,0,0,187,1,0,0,0,0,189,1,0,0,0,0,191, + 1,0,0,0,0,193,1,0,0,0,0,195,1,0,0,0,0,197,1,0,0,0,0,199,1,0,0,0, + 0,201,1,0,0,0,0,203,1,0,0,0,0,205,1,0,0,0,0,207,1,0,0,0,0,209,1, + 0,0,0,0,211,1,0,0,0,0,213,1,0,0,0,0,215,1,0,0,0,0,217,1,0,0,0,0, + 219,1,0,0,0,0,221,1,0,0,0,0,223,1,0,0,0,0,225,1,0,0,0,0,227,1,0, + 0,0,0,229,1,0,0,0,0,231,1,0,0,0,0,233,1,0,0,0,0,235,1,0,0,0,0,237, + 1,0,0,0,0,239,1,0,0,0,0,241,1,0,0,0,0,243,1,0,0,0,0,245,1,0,0,0, + 0,247,1,0,0,0,0,249,1,0,0,0,0,251,1,0,0,0,0,253,1,0,0,0,1,295,1, + 0,0,0,3,297,1,0,0,0,5,299,1,0,0,0,7,301,1,0,0,0,9,304,1,0,0,0,11, + 306,1,0,0,0,13,308,1,0,0,0,15,310,1,0,0,0,17,312,1,0,0,0,19,314, + 1,0,0,0,21,316,1,0,0,0,23,318,1,0,0,0,25,321,1,0,0,0,27,324,1,0, + 0,0,29,326,1,0,0,0,31,328,1,0,0,0,33,331,1,0,0,0,35,334,1,0,0,0, + 37,336,1,0,0,0,39,338,1,0,0,0,41,340,1,0,0,0,43,342,1,0,0,0,45,344, + 1,0,0,0,47,346,1,0,0,0,49,348,1,0,0,0,51,350,1,0,0,0,53,352,1,0, + 0,0,55,354,1,0,0,0,57,356,1,0,0,0,59,358,1,0,0,0,61,360,1,0,0,0, + 63,362,1,0,0,0,65,364,1,0,0,0,67,366,1,0,0,0,69,368,1,0,0,0,71,370, + 1,0,0,0,73,372,1,0,0,0,75,374,1,0,0,0,77,376,1,0,0,0,79,378,1,0, + 0,0,81,380,1,0,0,0,83,382,1,0,0,0,85,384,1,0,0,0,87,386,1,0,0,0, + 89,388,1,0,0,0,91,390,1,0,0,0,93,396,1,0,0,0,95,400,1,0,0,0,97,409, + 1,0,0,0,99,415,1,0,0,0,101,422,1,0,0,0,103,425,1,0,0,0,105,431,1, + 0,0,0,107,434,1,0,0,0,109,441,1,0,0,0,111,445,1,0,0,0,113,452,1, + 0,0,0,115,459,1,0,0,0,117,466,1,0,0,0,119,471,1,0,0,0,121,477,1, + 0,0,0,123,482,1,0,0,0,125,489,1,0,0,0,127,498,1,0,0,0,129,504,1, + 0,0,0,131,507,1,0,0,0,133,512,1,0,0,0,135,518,1,0,0,0,137,528,1, + 0,0,0,139,532,1,0,0,0,141,543,1,0,0,0,143,548,1,0,0,0,145,554,1, + 0,0,0,147,557,1,0,0,0,149,561,1,0,0,0,151,565,1,0,0,0,153,569,1, + 0,0,0,155,576,1,0,0,0,157,581,1,0,0,0,159,590,1,0,0,0,161,593,1, + 0,0,0,163,596,1,0,0,0,165,601,1,0,0,0,167,607,1,0,0,0,169,612,1, + 0,0,0,171,617,1,0,0,0,173,621,1,0,0,0,175,626,1,0,0,0,177,631,1, + 0,0,0,179,635,1,0,0,0,181,640,1,0,0,0,183,647,1,0,0,0,185,654,1, + 0,0,0,187,659,1,0,0,0,189,665,1,0,0,0,191,681,1,0,0,0,193,683,1, + 0,0,0,195,692,1,0,0,0,197,696,1,0,0,0,199,700,1,0,0,0,201,704,1, + 0,0,0,203,706,1,0,0,0,205,710,1,0,0,0,207,712,1,0,0,0,209,736,1, + 0,0,0,211,750,1,0,0,0,213,777,1,0,0,0,215,779,1,0,0,0,217,799,1, + 0,0,0,219,810,1,0,0,0,221,813,1,0,0,0,223,817,1,0,0,0,225,825,1, + 0,0,0,227,832,1,0,0,0,229,842,1,0,0,0,231,849,1,0,0,0,233,852,1, + 0,0,0,235,856,1,0,0,0,237,861,1,0,0,0,239,868,1,0,0,0,241,876,1, + 0,0,0,243,885,1,0,0,0,245,889,1,0,0,0,247,899,1,0,0,0,249,904,1, + 0,0,0,251,920,1,0,0,0,253,951,1,0,0,0,255,953,1,0,0,0,257,955,1, + 0,0,0,259,957,1,0,0,0,261,959,1,0,0,0,263,961,1,0,0,0,265,963,1, + 0,0,0,267,965,1,0,0,0,269,967,1,0,0,0,271,969,1,0,0,0,273,971,1, + 0,0,0,275,973,1,0,0,0,277,975,1,0,0,0,279,977,1,0,0,0,281,979,1, + 0,0,0,283,981,1,0,0,0,285,983,1,0,0,0,287,985,1,0,0,0,289,987,1, + 0,0,0,291,989,1,0,0,0,293,991,1,0,0,0,295,296,5,59,0,0,296,2,1,0, + 0,0,297,298,5,44,0,0,298,4,1,0,0,0,299,300,5,61,0,0,300,6,1,0,0, + 0,301,302,5,43,0,0,302,303,5,61,0,0,303,8,1,0,0,0,304,305,5,42,0, + 0,305,10,1,0,0,0,306,307,5,40,0,0,307,12,1,0,0,0,308,309,5,41,0, + 0,309,14,1,0,0,0,310,311,5,91,0,0,311,16,1,0,0,0,312,313,5,93,0, + 0,313,18,1,0,0,0,314,315,5,58,0,0,315,20,1,0,0,0,316,317,5,124,0, + 0,317,22,1,0,0,0,318,319,5,46,0,0,319,320,5,46,0,0,320,24,1,0,0, + 0,321,322,5,60,0,0,322,323,5,62,0,0,323,26,1,0,0,0,324,325,5,60, + 0,0,325,28,1,0,0,0,326,327,5,62,0,0,327,30,1,0,0,0,328,329,5,60, + 0,0,329,330,5,61,0,0,330,32,1,0,0,0,331,332,5,62,0,0,332,333,5,61, + 0,0,333,34,1,0,0,0,334,335,5,43,0,0,335,36,1,0,0,0,336,337,5,45, + 0,0,337,38,1,0,0,0,338,339,5,47,0,0,339,40,1,0,0,0,340,341,5,37, + 0,0,341,42,1,0,0,0,342,343,5,94,0,0,343,44,1,0,0,0,344,345,5,46, + 0,0,345,46,1,0,0,0,346,347,5,123,0,0,347,48,1,0,0,0,348,349,5,125, + 0,0,349,50,1,0,0,0,350,351,5,36,0,0,351,52,1,0,0,0,352,353,5,10216, + 0,0,353,54,1,0,0,0,354,355,5,12296,0,0,355,56,1,0,0,0,356,357,5, + 65124,0,0,357,58,1,0,0,0,358,359,5,65308,0,0,359,60,1,0,0,0,360, + 361,5,10217,0,0,361,62,1,0,0,0,362,363,5,12297,0,0,363,64,1,0,0, + 0,364,365,5,65125,0,0,365,66,1,0,0,0,366,367,5,65310,0,0,367,68, + 1,0,0,0,368,369,5,173,0,0,369,70,1,0,0,0,370,371,5,8208,0,0,371, + 72,1,0,0,0,372,373,5,8209,0,0,373,74,1,0,0,0,374,375,5,8210,0,0, + 375,76,1,0,0,0,376,377,5,8211,0,0,377,78,1,0,0,0,378,379,5,8212, + 0,0,379,80,1,0,0,0,380,381,5,8213,0,0,381,82,1,0,0,0,382,383,5,8722, + 0,0,383,84,1,0,0,0,384,385,5,65112,0,0,385,86,1,0,0,0,386,387,5, + 65123,0,0,387,88,1,0,0,0,388,389,5,65293,0,0,389,90,1,0,0,0,390, + 391,7,0,0,0,391,392,7,1,0,0,392,393,7,2,0,0,393,394,7,3,0,0,394, + 395,7,1,0,0,395,92,1,0,0,0,396,397,7,4,0,0,397,398,7,5,0,0,398,399, + 7,5,0,0,399,94,1,0,0,0,400,401,7,3,0,0,401,402,7,6,0,0,402,403,7, + 7,0,0,403,404,7,2,0,0,404,405,7,3,0,0,405,406,7,1,0,0,406,407,7, + 4,0,0,407,408,7,5,0,0,408,96,1,0,0,0,409,410,7,8,0,0,410,411,7,4, + 0,0,411,412,7,7,0,0,412,413,7,9,0,0,413,414,7,10,0,0,414,98,1,0, + 0,0,415,416,7,0,0,0,416,417,7,1,0,0,417,418,7,11,0,0,418,419,7,2, + 0,0,419,420,7,1,0,0,420,421,7,12,0,0,421,100,1,0,0,0,422,423,7,4, + 0,0,423,424,7,13,0,0,424,102,1,0,0,0,425,426,7,8,0,0,426,427,7,14, + 0,0,427,428,7,15,0,0,428,429,7,16,0,0,429,430,7,14,0,0,430,104,1, + 0,0,0,431,432,7,3,0,0,432,433,7,1,0,0,433,106,1,0,0,0,434,435,7, + 9,0,0,435,436,7,15,0,0,436,437,7,14,0,0,437,438,7,4,0,0,438,439, + 7,7,0,0,439,440,7,14,0,0,440,108,1,0,0,0,441,442,7,13,0,0,442,443, + 7,14,0,0,443,444,7,7,0,0,444,110,1,0,0,0,445,446,7,12,0,0,446,447, + 7,14,0,0,447,448,7,7,0,0,448,449,7,4,0,0,449,450,7,9,0,0,450,451, + 7,10,0,0,451,112,1,0,0,0,452,453,7,12,0,0,453,454,7,14,0,0,454,455, + 7,5,0,0,455,456,7,14,0,0,456,457,7,7,0,0,457,458,7,14,0,0,458,114, + 1,0,0,0,459,460,7,15,0,0,460,461,7,14,0,0,461,462,7,8,0,0,462,463, + 7,3,0,0,463,464,7,17,0,0,464,465,7,14,0,0,465,116,1,0,0,0,466,467, + 7,9,0,0,467,468,7,4,0,0,468,469,7,5,0,0,469,470,7,5,0,0,470,118, + 1,0,0,0,471,472,7,18,0,0,472,473,7,2,0,0,473,474,7,14,0,0,474,475, + 7,5,0,0,475,476,7,12,0,0,476,120,1,0,0,0,477,478,7,11,0,0,478,479, + 7,2,0,0,479,480,7,7,0,0,480,481,7,10,0,0,481,122,1,0,0,0,482,483, + 7,15,0,0,483,484,7,14,0,0,484,485,7,7,0,0,485,486,7,0,0,0,486,487, + 7,15,0,0,487,488,7,1,0,0,488,124,1,0,0,0,489,490,7,12,0,0,490,491, + 7,2,0,0,491,492,7,13,0,0,492,493,7,7,0,0,493,494,7,2,0,0,494,495, + 7,1,0,0,495,496,7,9,0,0,496,497,7,7,0,0,497,126,1,0,0,0,498,499, + 7,3,0,0,499,500,7,15,0,0,500,501,7,12,0,0,501,502,7,14,0,0,502,503, + 7,15,0,0,503,128,1,0,0,0,504,505,7,19,0,0,505,506,7,18,0,0,506,130, + 1,0,0,0,507,508,7,13,0,0,508,509,7,20,0,0,509,510,7,2,0,0,510,511, + 7,6,0,0,511,132,1,0,0,0,512,513,7,5,0,0,513,514,7,2,0,0,514,515, + 7,8,0,0,515,516,7,2,0,0,516,517,7,7,0,0,517,134,1,0,0,0,518,519, + 7,4,0,0,519,520,7,13,0,0,520,521,7,9,0,0,521,522,7,14,0,0,522,523, + 7,1,0,0,523,524,7,12,0,0,524,525,7,2,0,0,525,526,7,1,0,0,526,527, + 7,16,0,0,527,136,1,0,0,0,528,529,7,4,0,0,529,530,7,13,0,0,530,531, + 7,9,0,0,531,138,1,0,0,0,532,533,7,12,0,0,533,534,7,14,0,0,534,535, + 7,13,0,0,535,536,7,9,0,0,536,537,7,14,0,0,537,538,7,1,0,0,538,539, + 7,12,0,0,539,540,7,2,0,0,540,541,7,1,0,0,541,542,7,16,0,0,542,140, + 1,0,0,0,543,544,7,12,0,0,544,545,7,14,0,0,545,546,7,13,0,0,546,547, + 7,9,0,0,547,142,1,0,0,0,548,549,7,11,0,0,549,550,7,10,0,0,550,551, + 7,14,0,0,551,552,7,15,0,0,552,553,7,14,0,0,553,144,1,0,0,0,554,555, + 7,3,0,0,555,556,7,15,0,0,556,146,1,0,0,0,557,558,7,21,0,0,558,559, + 7,3,0,0,559,560,7,15,0,0,560,148,1,0,0,0,561,562,7,4,0,0,562,563, + 7,1,0,0,563,564,7,12,0,0,564,150,1,0,0,0,565,566,7,1,0,0,566,567, + 7,3,0,0,567,568,7,7,0,0,568,152,1,0,0,0,569,570,7,13,0,0,570,571, + 7,7,0,0,571,572,7,4,0,0,572,573,7,15,0,0,573,574,7,7,0,0,574,575, + 7,13,0,0,575,154,1,0,0,0,576,577,7,14,0,0,577,578,7,1,0,0,578,579, + 7,12,0,0,579,580,7,13,0,0,580,156,1,0,0,0,581,582,7,9,0,0,582,583, + 7,3,0,0,583,584,7,1,0,0,584,585,7,7,0,0,585,586,7,4,0,0,586,587, + 7,2,0,0,587,588,7,1,0,0,588,589,7,13,0,0,589,158,1,0,0,0,590,591, + 7,2,0,0,591,592,7,1,0,0,592,160,1,0,0,0,593,594,7,2,0,0,594,595, + 7,13,0,0,595,162,1,0,0,0,596,597,7,1,0,0,597,598,7,0,0,0,598,599, + 7,5,0,0,599,600,7,5,0,0,600,164,1,0,0,0,601,602,7,9,0,0,602,603, + 7,3,0,0,603,604,7,0,0,0,604,605,7,1,0,0,605,606,7,7,0,0,606,166, + 1,0,0,0,607,608,7,9,0,0,608,609,7,4,0,0,609,610,7,13,0,0,610,611, + 7,14,0,0,611,168,1,0,0,0,612,613,7,14,0,0,613,614,7,5,0,0,614,615, + 7,13,0,0,615,616,7,14,0,0,616,170,1,0,0,0,617,618,7,14,0,0,618,619, + 7,1,0,0,619,620,7,12,0,0,620,172,1,0,0,0,621,622,7,11,0,0,622,623, + 7,10,0,0,623,624,7,14,0,0,624,625,7,1,0,0,625,174,1,0,0,0,626,627, + 7,7,0,0,627,628,7,10,0,0,628,629,7,14,0,0,629,630,7,1,0,0,630,176, + 1,0,0,0,631,632,7,4,0,0,632,633,7,1,0,0,633,634,7,18,0,0,634,178, + 1,0,0,0,635,636,7,1,0,0,636,637,7,3,0,0,637,638,7,1,0,0,638,639, + 7,14,0,0,639,180,1,0,0,0,640,641,7,13,0,0,641,642,7,2,0,0,642,643, + 7,1,0,0,643,644,7,16,0,0,644,645,7,5,0,0,645,646,7,14,0,0,646,182, + 1,0,0,0,647,648,7,14,0,0,648,649,7,21,0,0,649,650,7,2,0,0,650,651, + 7,13,0,0,651,652,7,7,0,0,652,653,7,13,0,0,653,184,1,0,0,0,654,655, + 7,7,0,0,655,656,7,15,0,0,656,657,7,0,0,0,657,658,7,14,0,0,658,186, + 1,0,0,0,659,660,7,22,0,0,660,661,7,4,0,0,661,662,7,5,0,0,662,663, + 7,13,0,0,663,664,7,14,0,0,664,188,1,0,0,0,665,666,5,48,0,0,666,667, + 5,120,0,0,667,669,1,0,0,0,668,670,3,197,98,0,669,668,1,0,0,0,670, + 671,1,0,0,0,671,669,1,0,0,0,671,672,1,0,0,0,672,190,1,0,0,0,673, + 682,3,207,103,0,674,678,3,201,100,0,675,677,3,199,99,0,676,675,1, + 0,0,0,677,680,1,0,0,0,678,676,1,0,0,0,678,679,1,0,0,0,679,682,1, + 0,0,0,680,678,1,0,0,0,681,673,1,0,0,0,681,674,1,0,0,0,682,192,1, + 0,0,0,683,684,5,48,0,0,684,685,5,111,0,0,685,687,1,0,0,0,686,688, + 3,205,102,0,687,686,1,0,0,0,688,689,1,0,0,0,689,687,1,0,0,0,689, + 690,1,0,0,0,690,194,1,0,0,0,691,693,7,23,0,0,692,691,1,0,0,0,693, + 196,1,0,0,0,694,697,3,199,99,0,695,697,3,195,97,0,696,694,1,0,0, + 0,696,695,1,0,0,0,697,198,1,0,0,0,698,701,3,207,103,0,699,701,3, + 201,100,0,700,698,1,0,0,0,700,699,1,0,0,0,701,200,1,0,0,0,702,705, + 3,203,101,0,703,705,2,56,57,0,704,702,1,0,0,0,704,703,1,0,0,0,705, + 202,1,0,0,0,706,707,2,49,55,0,707,204,1,0,0,0,708,711,3,207,103, + 0,709,711,3,203,101,0,710,708,1,0,0,0,710,709,1,0,0,0,711,206,1, + 0,0,0,712,713,5,48,0,0,713,208,1,0,0,0,714,716,3,199,99,0,715,714, + 1,0,0,0,716,717,1,0,0,0,717,715,1,0,0,0,717,718,1,0,0,0,718,737, + 1,0,0,0,719,721,3,199,99,0,720,719,1,0,0,0,721,722,1,0,0,0,722,720, + 1,0,0,0,722,723,1,0,0,0,723,724,1,0,0,0,724,726,5,46,0,0,725,727, + 3,199,99,0,726,725,1,0,0,0,727,728,1,0,0,0,728,726,1,0,0,0,728,729, + 1,0,0,0,729,737,1,0,0,0,730,732,5,46,0,0,731,733,3,199,99,0,732, + 731,1,0,0,0,733,734,1,0,0,0,734,732,1,0,0,0,734,735,1,0,0,0,735, + 737,1,0,0,0,736,715,1,0,0,0,736,720,1,0,0,0,736,730,1,0,0,0,737, + 738,1,0,0,0,738,740,7,14,0,0,739,741,5,45,0,0,740,739,1,0,0,0,740, + 741,1,0,0,0,741,743,1,0,0,0,742,744,3,199,99,0,743,742,1,0,0,0,744, + 745,1,0,0,0,745,743,1,0,0,0,745,746,1,0,0,0,746,210,1,0,0,0,747, + 749,3,199,99,0,748,747,1,0,0,0,749,752,1,0,0,0,750,748,1,0,0,0,750, + 751,1,0,0,0,751,753,1,0,0,0,752,750,1,0,0,0,753,755,5,46,0,0,754, + 756,3,199,99,0,755,754,1,0,0,0,756,757,1,0,0,0,757,755,1,0,0,0,757, + 758,1,0,0,0,758,212,1,0,0,0,759,764,5,34,0,0,760,763,3,285,142,0, + 761,763,3,215,107,0,762,760,1,0,0,0,762,761,1,0,0,0,763,766,1,0, + 0,0,764,762,1,0,0,0,764,765,1,0,0,0,765,767,1,0,0,0,766,764,1,0, + 0,0,767,778,5,34,0,0,768,773,5,39,0,0,769,772,3,265,132,0,770,772, + 3,215,107,0,771,769,1,0,0,0,771,770,1,0,0,0,772,775,1,0,0,0,773, + 771,1,0,0,0,773,774,1,0,0,0,774,776,1,0,0,0,775,773,1,0,0,0,776, + 778,5,39,0,0,777,759,1,0,0,0,777,768,1,0,0,0,778,214,1,0,0,0,779, + 797,5,92,0,0,780,798,7,24,0,0,781,782,7,0,0,0,782,783,3,197,98,0, + 783,784,3,197,98,0,784,785,3,197,98,0,785,786,3,197,98,0,786,798, + 1,0,0,0,787,788,7,0,0,0,788,789,3,197,98,0,789,790,3,197,98,0,790, + 791,3,197,98,0,791,792,3,197,98,0,792,793,3,197,98,0,793,794,3,197, + 98,0,794,795,3,197,98,0,795,796,3,197,98,0,796,798,1,0,0,0,797,780, + 1,0,0,0,797,781,1,0,0,0,797,787,1,0,0,0,798,216,1,0,0,0,799,800, + 7,9,0,0,800,801,7,3,0,0,801,802,7,1,0,0,802,803,7,13,0,0,803,804, + 7,7,0,0,804,805,7,15,0,0,805,806,7,4,0,0,806,807,7,2,0,0,807,808, + 7,1,0,0,808,809,7,7,0,0,809,218,1,0,0,0,810,811,7,12,0,0,811,812, + 7,3,0,0,812,220,1,0,0,0,813,814,7,22,0,0,814,815,7,3,0,0,815,816, + 7,15,0,0,816,222,1,0,0,0,817,818,7,15,0,0,818,819,7,14,0,0,819,820, + 7,25,0,0,820,821,7,0,0,0,821,822,7,2,0,0,822,823,7,15,0,0,823,824, + 7,14,0,0,824,224,1,0,0,0,825,826,7,0,0,0,826,827,7,1,0,0,827,828, + 7,2,0,0,828,829,7,25,0,0,829,830,7,0,0,0,830,831,7,14,0,0,831,226, + 1,0,0,0,832,833,7,8,0,0,833,834,7,4,0,0,834,835,7,1,0,0,835,836, + 7,12,0,0,836,837,7,4,0,0,837,838,7,7,0,0,838,839,7,3,0,0,839,840, + 7,15,0,0,840,841,7,18,0,0,841,228,1,0,0,0,842,843,7,13,0,0,843,844, + 7,9,0,0,844,845,7,4,0,0,845,846,7,5,0,0,846,847,7,4,0,0,847,848, + 7,15,0,0,848,230,1,0,0,0,849,850,7,3,0,0,850,851,7,22,0,0,851,232, + 1,0,0,0,852,853,7,4,0,0,853,854,7,12,0,0,854,855,7,12,0,0,855,234, + 1,0,0,0,856,857,7,12,0,0,857,858,7,15,0,0,858,859,7,3,0,0,859,860, + 7,6,0,0,860,236,1,0,0,0,861,862,7,22,0,0,862,863,7,2,0,0,863,864, + 7,5,0,0,864,865,7,7,0,0,865,866,7,14,0,0,866,867,7,15,0,0,867,238, + 1,0,0,0,868,869,7,14,0,0,869,870,7,21,0,0,870,871,7,7,0,0,871,872, + 7,15,0,0,872,873,7,4,0,0,873,874,7,9,0,0,874,875,7,7,0,0,875,240, + 1,0,0,0,876,880,3,243,121,0,877,879,3,245,122,0,878,877,1,0,0,0, + 879,882,1,0,0,0,880,878,1,0,0,0,880,881,1,0,0,0,881,242,1,0,0,0, + 882,880,1,0,0,0,883,886,3,293,146,0,884,886,3,281,140,0,885,883, + 1,0,0,0,885,884,1,0,0,0,886,244,1,0,0,0,887,890,3,261,130,0,888, + 890,3,277,138,0,889,887,1,0,0,0,889,888,1,0,0,0,890,246,1,0,0,0, + 891,895,5,96,0,0,892,894,3,257,128,0,893,892,1,0,0,0,894,897,1,0, + 0,0,895,893,1,0,0,0,895,896,1,0,0,0,896,898,1,0,0,0,897,895,1,0, + 0,0,898,900,5,96,0,0,899,891,1,0,0,0,900,901,1,0,0,0,901,899,1,0, + 0,0,901,902,1,0,0,0,902,248,1,0,0,0,903,905,3,251,125,0,904,903, + 1,0,0,0,905,906,1,0,0,0,906,904,1,0,0,0,906,907,1,0,0,0,907,250, + 1,0,0,0,908,921,3,279,139,0,909,921,3,283,141,0,910,921,3,287,143, + 0,911,921,3,289,144,0,912,921,3,255,127,0,913,921,3,275,137,0,914, + 921,3,273,136,0,915,921,3,271,135,0,916,921,3,259,129,0,917,921, + 3,291,145,0,918,921,7,26,0,0,919,921,3,253,126,0,920,908,1,0,0,0, + 920,909,1,0,0,0,920,910,1,0,0,0,920,911,1,0,0,0,920,912,1,0,0,0, + 920,913,1,0,0,0,920,914,1,0,0,0,920,915,1,0,0,0,920,916,1,0,0,0, + 920,917,1,0,0,0,920,918,1,0,0,0,920,919,1,0,0,0,921,252,1,0,0,0, + 922,923,5,47,0,0,923,924,5,42,0,0,924,930,1,0,0,0,925,929,3,263, + 131,0,926,927,5,42,0,0,927,929,3,269,134,0,928,925,1,0,0,0,928,926, + 1,0,0,0,929,932,1,0,0,0,930,928,1,0,0,0,930,931,1,0,0,0,931,933, + 1,0,0,0,932,930,1,0,0,0,933,934,5,42,0,0,934,952,5,47,0,0,935,936, + 5,47,0,0,936,937,5,47,0,0,937,941,1,0,0,0,938,940,3,267,133,0,939, + 938,1,0,0,0,940,943,1,0,0,0,941,939,1,0,0,0,941,942,1,0,0,0,942, + 945,1,0,0,0,943,941,1,0,0,0,944,946,3,275,137,0,945,944,1,0,0,0, + 945,946,1,0,0,0,946,949,1,0,0,0,947,950,3,287,143,0,948,950,5,0, + 0,1,949,947,1,0,0,0,949,948,1,0,0,0,950,952,1,0,0,0,951,922,1,0, + 0,0,951,935,1,0,0,0,952,254,1,0,0,0,953,954,7,27,0,0,954,256,1,0, + 0,0,955,956,8,28,0,0,956,258,1,0,0,0,957,958,7,29,0,0,958,260,1, + 0,0,0,959,960,7,30,0,0,960,262,1,0,0,0,961,962,8,31,0,0,962,264, + 1,0,0,0,963,964,8,32,0,0,964,266,1,0,0,0,965,966,8,33,0,0,966,268, + 1,0,0,0,967,968,8,34,0,0,968,270,1,0,0,0,969,970,7,35,0,0,970,272, + 1,0,0,0,971,972,7,36,0,0,972,274,1,0,0,0,973,974,7,37,0,0,974,276, + 1,0,0,0,975,976,7,38,0,0,976,278,1,0,0,0,977,978,7,39,0,0,978,280, + 1,0,0,0,979,980,7,40,0,0,980,282,1,0,0,0,981,982,7,41,0,0,982,284, + 1,0,0,0,983,984,8,42,0,0,984,286,1,0,0,0,985,986,7,43,0,0,986,288, + 1,0,0,0,987,988,7,44,0,0,988,290,1,0,0,0,989,990,7,45,0,0,990,292, + 1,0,0,0,991,992,7,46,0,0,992,294,1,0,0,0,38,0,671,678,681,689,692, + 696,700,704,710,717,722,728,734,736,740,745,750,757,762,764,771, + 773,777,797,880,885,889,895,901,906,920,928,930,941,945,949,951, + 0 + ] + +class CypherLexer(Lexer): + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + T__0 = 1 + T__1 = 2 + T__2 = 3 + T__3 = 4 + T__4 = 5 + T__5 = 6 + T__6 = 7 + T__7 = 8 + T__8 = 9 + T__9 = 10 + T__10 = 11 + T__11 = 12 + T__12 = 13 + T__13 = 14 + T__14 = 15 + T__15 = 16 + T__16 = 17 + T__17 = 18 + T__18 = 19 + T__19 = 20 + T__20 = 21 + T__21 = 22 + T__22 = 23 + T__23 = 24 + T__24 = 25 + T__25 = 26 + T__26 = 27 + T__27 = 28 + T__28 = 29 + T__29 = 30 + T__30 = 31 + T__31 = 32 + T__32 = 33 + T__33 = 34 + T__34 = 35 + T__35 = 36 + T__36 = 37 + T__37 = 38 + T__38 = 39 + T__39 = 40 + T__40 = 41 + T__41 = 42 + T__42 = 43 + T__43 = 44 + T__44 = 45 + UNION = 46 + ALL = 47 + OPTIONAL = 48 + MATCH = 49 + UNWIND = 50 + AS = 51 + MERGE = 52 + ON = 53 + CREATE = 54 + SET = 55 + DETACH = 56 + DELETE = 57 + REMOVE = 58 + CALL = 59 + YIELD = 60 + WITH = 61 + RETURN = 62 + DISTINCT = 63 + ORDER = 64 + BY = 65 + L_SKIP = 66 + LIMIT = 67 + ASCENDING = 68 + ASC = 69 + DESCENDING = 70 + DESC = 71 + WHERE = 72 + OR = 73 + XOR = 74 + AND = 75 + NOT = 76 + STARTS = 77 + ENDS = 78 + CONTAINS = 79 + IN = 80 + IS = 81 + NULL = 82 + COUNT = 83 + CASE = 84 + ELSE = 85 + END = 86 + WHEN = 87 + THEN = 88 + ANY = 89 + NONE = 90 + SINGLE = 91 + EXISTS = 92 + TRUE = 93 + FALSE = 94 + HexInteger = 95 + DecimalInteger = 96 + OctalInteger = 97 + HexLetter = 98 + HexDigit = 99 + Digit = 100 + NonZeroDigit = 101 + NonZeroOctDigit = 102 + OctDigit = 103 + ZeroDigit = 104 + ExponentDecimalReal = 105 + RegularDecimalReal = 106 + StringLiteral = 107 + EscapedChar = 108 + CONSTRAINT = 109 + DO = 110 + FOR = 111 + REQUIRE = 112 + UNIQUE = 113 + MANDATORY = 114 + SCALAR = 115 + OF = 116 + ADD = 117 + DROP = 118 + FILTER = 119 + EXTRACT = 120 + UnescapedSymbolicName = 121 + IdentifierStart = 122 + IdentifierPart = 123 + EscapedSymbolicName = 124 + SP = 125 + WHITESPACE = 126 + Comment = 127 + + channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ] + + modeNames = [ "DEFAULT_MODE" ] + + literalNames = [ "", + "';'", "','", "'='", "'+='", "'*'", "'('", "')'", "'['", "']'", + "':'", "'|'", "'..'", "'<>'", "'<'", "'>'", "'<='", "'>='", + "'+'", "'-'", "'/'", "'%'", "'^'", "'.'", "'{'", "'}'", "'$'", + "'\\u27E8'", "'\\u3008'", "'\\uFE64'", "'\\uFF1C'", "'\\u27E9'", + "'\\u3009'", "'\\uFE65'", "'\\uFF1E'", "'\\u00AD'", "'\\u2010'", + "'\\u2011'", "'\\u2012'", "'\\u2013'", "'\\u2014'", "'\\u2015'", + "'\\u2212'", "'\\uFE58'", "'\\uFE63'", "'\\uFF0D'", "'0'" ] + + symbolicNames = [ "", + "UNION", "ALL", "OPTIONAL", "MATCH", "UNWIND", "AS", "MERGE", + "ON", "CREATE", "SET", "DETACH", "DELETE", "REMOVE", "CALL", + "YIELD", "WITH", "RETURN", "DISTINCT", "ORDER", "BY", "L_SKIP", + "LIMIT", "ASCENDING", "ASC", "DESCENDING", "DESC", "WHERE", + "OR", "XOR", "AND", "NOT", "STARTS", "ENDS", "CONTAINS", "IN", + "IS", "NULL", "COUNT", "CASE", "ELSE", "END", "WHEN", "THEN", + "ANY", "NONE", "SINGLE", "EXISTS", "TRUE", "FALSE", "HexInteger", + "DecimalInteger", "OctalInteger", "HexLetter", "HexDigit", "Digit", + "NonZeroDigit", "NonZeroOctDigit", "OctDigit", "ZeroDigit", + "ExponentDecimalReal", "RegularDecimalReal", "StringLiteral", + "EscapedChar", "CONSTRAINT", "DO", "FOR", "REQUIRE", "UNIQUE", + "MANDATORY", "SCALAR", "OF", "ADD", "DROP", "FILTER", "EXTRACT", + "UnescapedSymbolicName", "IdentifierStart", "IdentifierPart", + "EscapedSymbolicName", "SP", "WHITESPACE", "Comment" ] + + ruleNames = [ "T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6", + "T__7", "T__8", "T__9", "T__10", "T__11", "T__12", "T__13", + "T__14", "T__15", "T__16", "T__17", "T__18", "T__19", + "T__20", "T__21", "T__22", "T__23", "T__24", "T__25", + "T__26", "T__27", "T__28", "T__29", "T__30", "T__31", + "T__32", "T__33", "T__34", "T__35", "T__36", "T__37", + "T__38", "T__39", "T__40", "T__41", "T__42", "T__43", + "T__44", "UNION", "ALL", "OPTIONAL", "MATCH", "UNWIND", + "AS", "MERGE", "ON", "CREATE", "SET", "DETACH", "DELETE", + "REMOVE", "CALL", "YIELD", "WITH", "RETURN", "DISTINCT", + "ORDER", "BY", "L_SKIP", "LIMIT", "ASCENDING", "ASC", + "DESCENDING", "DESC", "WHERE", "OR", "XOR", "AND", "NOT", + "STARTS", "ENDS", "CONTAINS", "IN", "IS", "NULL", "COUNT", + "CASE", "ELSE", "END", "WHEN", "THEN", "ANY", "NONE", + "SINGLE", "EXISTS", "TRUE", "FALSE", "HexInteger", "DecimalInteger", + "OctalInteger", "HexLetter", "HexDigit", "Digit", "NonZeroDigit", + "NonZeroOctDigit", "OctDigit", "ZeroDigit", "ExponentDecimalReal", + "RegularDecimalReal", "StringLiteral", "EscapedChar", + "CONSTRAINT", "DO", "FOR", "REQUIRE", "UNIQUE", "MANDATORY", + "SCALAR", "OF", "ADD", "DROP", "FILTER", "EXTRACT", "UnescapedSymbolicName", + "IdentifierStart", "IdentifierPart", "EscapedSymbolicName", + "SP", "WHITESPACE", "Comment", "FF", "EscapedSymbolicName_0", + "RS", "ID_Continue", "Comment_1", "StringLiteral_1", "Comment_3", + "Comment_2", "GS", "FS", "CR", "Sc", "SPACE", "Pc", "TAB", + "StringLiteral_0", "LF", "VT", "US", "ID_Start" ] + + grammarFileName = "Cypher.g4" + + def __init__(self, input=None, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.13.2") + self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache()) + self._actions = None + self._predicates = None + + diff --git a/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherListener.py b/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherListener.py new file mode 100644 index 000000000..88ea5db13 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherListener.py @@ -0,0 +1,939 @@ +# Generated from Cypher.g4 by ANTLR 4.13.2 +from antlr4 import * +if "." in __name__: + from .CypherParser import CypherParser +else: + from CypherParser import CypherParser + +# This class defines a complete listener for a parse tree produced by CypherParser. +class CypherListener(ParseTreeListener): + + # Enter a parse tree produced by CypherParser#oC_Cypher. + def enterOC_Cypher(self, ctx:CypherParser.OC_CypherContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Cypher. + def exitOC_Cypher(self, ctx:CypherParser.OC_CypherContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Statement. + def enterOC_Statement(self, ctx:CypherParser.OC_StatementContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Statement. + def exitOC_Statement(self, ctx:CypherParser.OC_StatementContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Query. + def enterOC_Query(self, ctx:CypherParser.OC_QueryContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Query. + def exitOC_Query(self, ctx:CypherParser.OC_QueryContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_RegularQuery. + def enterOC_RegularQuery(self, ctx:CypherParser.OC_RegularQueryContext): + pass + + # Exit a parse tree produced by CypherParser#oC_RegularQuery. + def exitOC_RegularQuery(self, ctx:CypherParser.OC_RegularQueryContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Union. + def enterOC_Union(self, ctx:CypherParser.OC_UnionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Union. + def exitOC_Union(self, ctx:CypherParser.OC_UnionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_SingleQuery. + def enterOC_SingleQuery(self, ctx:CypherParser.OC_SingleQueryContext): + pass + + # Exit a parse tree produced by CypherParser#oC_SingleQuery. + def exitOC_SingleQuery(self, ctx:CypherParser.OC_SingleQueryContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_SinglePartQuery. + def enterOC_SinglePartQuery(self, ctx:CypherParser.OC_SinglePartQueryContext): + pass + + # Exit a parse tree produced by CypherParser#oC_SinglePartQuery. + def exitOC_SinglePartQuery(self, ctx:CypherParser.OC_SinglePartQueryContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_MultiPartQuery. + def enterOC_MultiPartQuery(self, ctx:CypherParser.OC_MultiPartQueryContext): + pass + + # Exit a parse tree produced by CypherParser#oC_MultiPartQuery. + def exitOC_MultiPartQuery(self, ctx:CypherParser.OC_MultiPartQueryContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_UpdatingClause. + def enterOC_UpdatingClause(self, ctx:CypherParser.OC_UpdatingClauseContext): + pass + + # Exit a parse tree produced by CypherParser#oC_UpdatingClause. + def exitOC_UpdatingClause(self, ctx:CypherParser.OC_UpdatingClauseContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ReadingClause. + def enterOC_ReadingClause(self, ctx:CypherParser.OC_ReadingClauseContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ReadingClause. + def exitOC_ReadingClause(self, ctx:CypherParser.OC_ReadingClauseContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Match. + def enterOC_Match(self, ctx:CypherParser.OC_MatchContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Match. + def exitOC_Match(self, ctx:CypherParser.OC_MatchContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Unwind. + def enterOC_Unwind(self, ctx:CypherParser.OC_UnwindContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Unwind. + def exitOC_Unwind(self, ctx:CypherParser.OC_UnwindContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Merge. + def enterOC_Merge(self, ctx:CypherParser.OC_MergeContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Merge. + def exitOC_Merge(self, ctx:CypherParser.OC_MergeContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_MergeAction. + def enterOC_MergeAction(self, ctx:CypherParser.OC_MergeActionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_MergeAction. + def exitOC_MergeAction(self, ctx:CypherParser.OC_MergeActionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Create. + def enterOC_Create(self, ctx:CypherParser.OC_CreateContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Create. + def exitOC_Create(self, ctx:CypherParser.OC_CreateContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Set. + def enterOC_Set(self, ctx:CypherParser.OC_SetContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Set. + def exitOC_Set(self, ctx:CypherParser.OC_SetContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_SetItem. + def enterOC_SetItem(self, ctx:CypherParser.OC_SetItemContext): + pass + + # Exit a parse tree produced by CypherParser#oC_SetItem. + def exitOC_SetItem(self, ctx:CypherParser.OC_SetItemContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Delete. + def enterOC_Delete(self, ctx:CypherParser.OC_DeleteContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Delete. + def exitOC_Delete(self, ctx:CypherParser.OC_DeleteContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Remove. + def enterOC_Remove(self, ctx:CypherParser.OC_RemoveContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Remove. + def exitOC_Remove(self, ctx:CypherParser.OC_RemoveContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_RemoveItem. + def enterOC_RemoveItem(self, ctx:CypherParser.OC_RemoveItemContext): + pass + + # Exit a parse tree produced by CypherParser#oC_RemoveItem. + def exitOC_RemoveItem(self, ctx:CypherParser.OC_RemoveItemContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_InQueryCall. + def enterOC_InQueryCall(self, ctx:CypherParser.OC_InQueryCallContext): + pass + + # Exit a parse tree produced by CypherParser#oC_InQueryCall. + def exitOC_InQueryCall(self, ctx:CypherParser.OC_InQueryCallContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_StandaloneCall. + def enterOC_StandaloneCall(self, ctx:CypherParser.OC_StandaloneCallContext): + pass + + # Exit a parse tree produced by CypherParser#oC_StandaloneCall. + def exitOC_StandaloneCall(self, ctx:CypherParser.OC_StandaloneCallContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_YieldItems. + def enterOC_YieldItems(self, ctx:CypherParser.OC_YieldItemsContext): + pass + + # Exit a parse tree produced by CypherParser#oC_YieldItems. + def exitOC_YieldItems(self, ctx:CypherParser.OC_YieldItemsContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_YieldItem. + def enterOC_YieldItem(self, ctx:CypherParser.OC_YieldItemContext): + pass + + # Exit a parse tree produced by CypherParser#oC_YieldItem. + def exitOC_YieldItem(self, ctx:CypherParser.OC_YieldItemContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_With. + def enterOC_With(self, ctx:CypherParser.OC_WithContext): + pass + + # Exit a parse tree produced by CypherParser#oC_With. + def exitOC_With(self, ctx:CypherParser.OC_WithContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Return. + def enterOC_Return(self, ctx:CypherParser.OC_ReturnContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Return. + def exitOC_Return(self, ctx:CypherParser.OC_ReturnContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ProjectionBody. + def enterOC_ProjectionBody(self, ctx:CypherParser.OC_ProjectionBodyContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ProjectionBody. + def exitOC_ProjectionBody(self, ctx:CypherParser.OC_ProjectionBodyContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ProjectionItems. + def enterOC_ProjectionItems(self, ctx:CypherParser.OC_ProjectionItemsContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ProjectionItems. + def exitOC_ProjectionItems(self, ctx:CypherParser.OC_ProjectionItemsContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ProjectionItem. + def enterOC_ProjectionItem(self, ctx:CypherParser.OC_ProjectionItemContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ProjectionItem. + def exitOC_ProjectionItem(self, ctx:CypherParser.OC_ProjectionItemContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Order. + def enterOC_Order(self, ctx:CypherParser.OC_OrderContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Order. + def exitOC_Order(self, ctx:CypherParser.OC_OrderContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Skip. + def enterOC_Skip(self, ctx:CypherParser.OC_SkipContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Skip. + def exitOC_Skip(self, ctx:CypherParser.OC_SkipContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Limit. + def enterOC_Limit(self, ctx:CypherParser.OC_LimitContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Limit. + def exitOC_Limit(self, ctx:CypherParser.OC_LimitContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_SortItem. + def enterOC_SortItem(self, ctx:CypherParser.OC_SortItemContext): + pass + + # Exit a parse tree produced by CypherParser#oC_SortItem. + def exitOC_SortItem(self, ctx:CypherParser.OC_SortItemContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Where. + def enterOC_Where(self, ctx:CypherParser.OC_WhereContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Where. + def exitOC_Where(self, ctx:CypherParser.OC_WhereContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Pattern. + def enterOC_Pattern(self, ctx:CypherParser.OC_PatternContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Pattern. + def exitOC_Pattern(self, ctx:CypherParser.OC_PatternContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_PatternPart. + def enterOC_PatternPart(self, ctx:CypherParser.OC_PatternPartContext): + pass + + # Exit a parse tree produced by CypherParser#oC_PatternPart. + def exitOC_PatternPart(self, ctx:CypherParser.OC_PatternPartContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_AnonymousPatternPart. + def enterOC_AnonymousPatternPart(self, ctx:CypherParser.OC_AnonymousPatternPartContext): + pass + + # Exit a parse tree produced by CypherParser#oC_AnonymousPatternPart. + def exitOC_AnonymousPatternPart(self, ctx:CypherParser.OC_AnonymousPatternPartContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_PatternElement. + def enterOC_PatternElement(self, ctx:CypherParser.OC_PatternElementContext): + pass + + # Exit a parse tree produced by CypherParser#oC_PatternElement. + def exitOC_PatternElement(self, ctx:CypherParser.OC_PatternElementContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_RelationshipsPattern. + def enterOC_RelationshipsPattern(self, ctx:CypherParser.OC_RelationshipsPatternContext): + pass + + # Exit a parse tree produced by CypherParser#oC_RelationshipsPattern. + def exitOC_RelationshipsPattern(self, ctx:CypherParser.OC_RelationshipsPatternContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_NodePattern. + def enterOC_NodePattern(self, ctx:CypherParser.OC_NodePatternContext): + pass + + # Exit a parse tree produced by CypherParser#oC_NodePattern. + def exitOC_NodePattern(self, ctx:CypherParser.OC_NodePatternContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_PatternElementChain. + def enterOC_PatternElementChain(self, ctx:CypherParser.OC_PatternElementChainContext): + pass + + # Exit a parse tree produced by CypherParser#oC_PatternElementChain. + def exitOC_PatternElementChain(self, ctx:CypherParser.OC_PatternElementChainContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_RelationshipPattern. + def enterOC_RelationshipPattern(self, ctx:CypherParser.OC_RelationshipPatternContext): + pass + + # Exit a parse tree produced by CypherParser#oC_RelationshipPattern. + def exitOC_RelationshipPattern(self, ctx:CypherParser.OC_RelationshipPatternContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_RelationshipDetail. + def enterOC_RelationshipDetail(self, ctx:CypherParser.OC_RelationshipDetailContext): + pass + + # Exit a parse tree produced by CypherParser#oC_RelationshipDetail. + def exitOC_RelationshipDetail(self, ctx:CypherParser.OC_RelationshipDetailContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Properties. + def enterOC_Properties(self, ctx:CypherParser.OC_PropertiesContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Properties. + def exitOC_Properties(self, ctx:CypherParser.OC_PropertiesContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_RelationshipTypes. + def enterOC_RelationshipTypes(self, ctx:CypherParser.OC_RelationshipTypesContext): + pass + + # Exit a parse tree produced by CypherParser#oC_RelationshipTypes. + def exitOC_RelationshipTypes(self, ctx:CypherParser.OC_RelationshipTypesContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_NodeLabels. + def enterOC_NodeLabels(self, ctx:CypherParser.OC_NodeLabelsContext): + pass + + # Exit a parse tree produced by CypherParser#oC_NodeLabels. + def exitOC_NodeLabels(self, ctx:CypherParser.OC_NodeLabelsContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_NodeLabel. + def enterOC_NodeLabel(self, ctx:CypherParser.OC_NodeLabelContext): + pass + + # Exit a parse tree produced by CypherParser#oC_NodeLabel. + def exitOC_NodeLabel(self, ctx:CypherParser.OC_NodeLabelContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_RangeLiteral. + def enterOC_RangeLiteral(self, ctx:CypherParser.OC_RangeLiteralContext): + pass + + # Exit a parse tree produced by CypherParser#oC_RangeLiteral. + def exitOC_RangeLiteral(self, ctx:CypherParser.OC_RangeLiteralContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_LabelName. + def enterOC_LabelName(self, ctx:CypherParser.OC_LabelNameContext): + pass + + # Exit a parse tree produced by CypherParser#oC_LabelName. + def exitOC_LabelName(self, ctx:CypherParser.OC_LabelNameContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_RelTypeName. + def enterOC_RelTypeName(self, ctx:CypherParser.OC_RelTypeNameContext): + pass + + # Exit a parse tree produced by CypherParser#oC_RelTypeName. + def exitOC_RelTypeName(self, ctx:CypherParser.OC_RelTypeNameContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_PropertyExpression. + def enterOC_PropertyExpression(self, ctx:CypherParser.OC_PropertyExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_PropertyExpression. + def exitOC_PropertyExpression(self, ctx:CypherParser.OC_PropertyExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Expression. + def enterOC_Expression(self, ctx:CypherParser.OC_ExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Expression. + def exitOC_Expression(self, ctx:CypherParser.OC_ExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_OrExpression. + def enterOC_OrExpression(self, ctx:CypherParser.OC_OrExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_OrExpression. + def exitOC_OrExpression(self, ctx:CypherParser.OC_OrExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_XorExpression. + def enterOC_XorExpression(self, ctx:CypherParser.OC_XorExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_XorExpression. + def exitOC_XorExpression(self, ctx:CypherParser.OC_XorExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_AndExpression. + def enterOC_AndExpression(self, ctx:CypherParser.OC_AndExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_AndExpression. + def exitOC_AndExpression(self, ctx:CypherParser.OC_AndExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_NotExpression. + def enterOC_NotExpression(self, ctx:CypherParser.OC_NotExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_NotExpression. + def exitOC_NotExpression(self, ctx:CypherParser.OC_NotExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ComparisonExpression. + def enterOC_ComparisonExpression(self, ctx:CypherParser.OC_ComparisonExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ComparisonExpression. + def exitOC_ComparisonExpression(self, ctx:CypherParser.OC_ComparisonExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_PartialComparisonExpression. + def enterOC_PartialComparisonExpression(self, ctx:CypherParser.OC_PartialComparisonExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_PartialComparisonExpression. + def exitOC_PartialComparisonExpression(self, ctx:CypherParser.OC_PartialComparisonExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_StringListNullPredicateExpression. + def enterOC_StringListNullPredicateExpression(self, ctx:CypherParser.OC_StringListNullPredicateExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_StringListNullPredicateExpression. + def exitOC_StringListNullPredicateExpression(self, ctx:CypherParser.OC_StringListNullPredicateExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_StringPredicateExpression. + def enterOC_StringPredicateExpression(self, ctx:CypherParser.OC_StringPredicateExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_StringPredicateExpression. + def exitOC_StringPredicateExpression(self, ctx:CypherParser.OC_StringPredicateExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ListPredicateExpression. + def enterOC_ListPredicateExpression(self, ctx:CypherParser.OC_ListPredicateExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ListPredicateExpression. + def exitOC_ListPredicateExpression(self, ctx:CypherParser.OC_ListPredicateExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_NullPredicateExpression. + def enterOC_NullPredicateExpression(self, ctx:CypherParser.OC_NullPredicateExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_NullPredicateExpression. + def exitOC_NullPredicateExpression(self, ctx:CypherParser.OC_NullPredicateExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_AddOrSubtractExpression. + def enterOC_AddOrSubtractExpression(self, ctx:CypherParser.OC_AddOrSubtractExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_AddOrSubtractExpression. + def exitOC_AddOrSubtractExpression(self, ctx:CypherParser.OC_AddOrSubtractExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_MultiplyDivideModuloExpression. + def enterOC_MultiplyDivideModuloExpression(self, ctx:CypherParser.OC_MultiplyDivideModuloExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_MultiplyDivideModuloExpression. + def exitOC_MultiplyDivideModuloExpression(self, ctx:CypherParser.OC_MultiplyDivideModuloExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_PowerOfExpression. + def enterOC_PowerOfExpression(self, ctx:CypherParser.OC_PowerOfExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_PowerOfExpression. + def exitOC_PowerOfExpression(self, ctx:CypherParser.OC_PowerOfExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_UnaryAddOrSubtractExpression. + def enterOC_UnaryAddOrSubtractExpression(self, ctx:CypherParser.OC_UnaryAddOrSubtractExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_UnaryAddOrSubtractExpression. + def exitOC_UnaryAddOrSubtractExpression(self, ctx:CypherParser.OC_UnaryAddOrSubtractExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_NonArithmeticOperatorExpression. + def enterOC_NonArithmeticOperatorExpression(self, ctx:CypherParser.OC_NonArithmeticOperatorExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_NonArithmeticOperatorExpression. + def exitOC_NonArithmeticOperatorExpression(self, ctx:CypherParser.OC_NonArithmeticOperatorExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ListOperatorExpression. + def enterOC_ListOperatorExpression(self, ctx:CypherParser.OC_ListOperatorExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ListOperatorExpression. + def exitOC_ListOperatorExpression(self, ctx:CypherParser.OC_ListOperatorExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_PropertyLookup. + def enterOC_PropertyLookup(self, ctx:CypherParser.OC_PropertyLookupContext): + pass + + # Exit a parse tree produced by CypherParser#oC_PropertyLookup. + def exitOC_PropertyLookup(self, ctx:CypherParser.OC_PropertyLookupContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Atom. + def enterOC_Atom(self, ctx:CypherParser.OC_AtomContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Atom. + def exitOC_Atom(self, ctx:CypherParser.OC_AtomContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_CaseExpression. + def enterOC_CaseExpression(self, ctx:CypherParser.OC_CaseExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_CaseExpression. + def exitOC_CaseExpression(self, ctx:CypherParser.OC_CaseExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_CaseAlternative. + def enterOC_CaseAlternative(self, ctx:CypherParser.OC_CaseAlternativeContext): + pass + + # Exit a parse tree produced by CypherParser#oC_CaseAlternative. + def exitOC_CaseAlternative(self, ctx:CypherParser.OC_CaseAlternativeContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ListComprehension. + def enterOC_ListComprehension(self, ctx:CypherParser.OC_ListComprehensionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ListComprehension. + def exitOC_ListComprehension(self, ctx:CypherParser.OC_ListComprehensionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_PatternComprehension. + def enterOC_PatternComprehension(self, ctx:CypherParser.OC_PatternComprehensionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_PatternComprehension. + def exitOC_PatternComprehension(self, ctx:CypherParser.OC_PatternComprehensionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Quantifier. + def enterOC_Quantifier(self, ctx:CypherParser.OC_QuantifierContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Quantifier. + def exitOC_Quantifier(self, ctx:CypherParser.OC_QuantifierContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_FilterExpression. + def enterOC_FilterExpression(self, ctx:CypherParser.OC_FilterExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_FilterExpression. + def exitOC_FilterExpression(self, ctx:CypherParser.OC_FilterExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_PatternPredicate. + def enterOC_PatternPredicate(self, ctx:CypherParser.OC_PatternPredicateContext): + pass + + # Exit a parse tree produced by CypherParser#oC_PatternPredicate. + def exitOC_PatternPredicate(self, ctx:CypherParser.OC_PatternPredicateContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ParenthesizedExpression. + def enterOC_ParenthesizedExpression(self, ctx:CypherParser.OC_ParenthesizedExpressionContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ParenthesizedExpression. + def exitOC_ParenthesizedExpression(self, ctx:CypherParser.OC_ParenthesizedExpressionContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_IdInColl. + def enterOC_IdInColl(self, ctx:CypherParser.OC_IdInCollContext): + pass + + # Exit a parse tree produced by CypherParser#oC_IdInColl. + def exitOC_IdInColl(self, ctx:CypherParser.OC_IdInCollContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_FunctionInvocation. + def enterOC_FunctionInvocation(self, ctx:CypherParser.OC_FunctionInvocationContext): + pass + + # Exit a parse tree produced by CypherParser#oC_FunctionInvocation. + def exitOC_FunctionInvocation(self, ctx:CypherParser.OC_FunctionInvocationContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_FunctionName. + def enterOC_FunctionName(self, ctx:CypherParser.OC_FunctionNameContext): + pass + + # Exit a parse tree produced by CypherParser#oC_FunctionName. + def exitOC_FunctionName(self, ctx:CypherParser.OC_FunctionNameContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ExistentialSubquery. + def enterOC_ExistentialSubquery(self, ctx:CypherParser.OC_ExistentialSubqueryContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ExistentialSubquery. + def exitOC_ExistentialSubquery(self, ctx:CypherParser.OC_ExistentialSubqueryContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ExplicitProcedureInvocation. + def enterOC_ExplicitProcedureInvocation(self, ctx:CypherParser.OC_ExplicitProcedureInvocationContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ExplicitProcedureInvocation. + def exitOC_ExplicitProcedureInvocation(self, ctx:CypherParser.OC_ExplicitProcedureInvocationContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ImplicitProcedureInvocation. + def enterOC_ImplicitProcedureInvocation(self, ctx:CypherParser.OC_ImplicitProcedureInvocationContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ImplicitProcedureInvocation. + def exitOC_ImplicitProcedureInvocation(self, ctx:CypherParser.OC_ImplicitProcedureInvocationContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ProcedureResultField. + def enterOC_ProcedureResultField(self, ctx:CypherParser.OC_ProcedureResultFieldContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ProcedureResultField. + def exitOC_ProcedureResultField(self, ctx:CypherParser.OC_ProcedureResultFieldContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ProcedureName. + def enterOC_ProcedureName(self, ctx:CypherParser.OC_ProcedureNameContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ProcedureName. + def exitOC_ProcedureName(self, ctx:CypherParser.OC_ProcedureNameContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Namespace. + def enterOC_Namespace(self, ctx:CypherParser.OC_NamespaceContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Namespace. + def exitOC_Namespace(self, ctx:CypherParser.OC_NamespaceContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Variable. + def enterOC_Variable(self, ctx:CypherParser.OC_VariableContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Variable. + def exitOC_Variable(self, ctx:CypherParser.OC_VariableContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Literal. + def enterOC_Literal(self, ctx:CypherParser.OC_LiteralContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Literal. + def exitOC_Literal(self, ctx:CypherParser.OC_LiteralContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_BooleanLiteral. + def enterOC_BooleanLiteral(self, ctx:CypherParser.OC_BooleanLiteralContext): + pass + + # Exit a parse tree produced by CypherParser#oC_BooleanLiteral. + def exitOC_BooleanLiteral(self, ctx:CypherParser.OC_BooleanLiteralContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_NumberLiteral. + def enterOC_NumberLiteral(self, ctx:CypherParser.OC_NumberLiteralContext): + pass + + # Exit a parse tree produced by CypherParser#oC_NumberLiteral. + def exitOC_NumberLiteral(self, ctx:CypherParser.OC_NumberLiteralContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_IntegerLiteral. + def enterOC_IntegerLiteral(self, ctx:CypherParser.OC_IntegerLiteralContext): + pass + + # Exit a parse tree produced by CypherParser#oC_IntegerLiteral. + def exitOC_IntegerLiteral(self, ctx:CypherParser.OC_IntegerLiteralContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_DoubleLiteral. + def enterOC_DoubleLiteral(self, ctx:CypherParser.OC_DoubleLiteralContext): + pass + + # Exit a parse tree produced by CypherParser#oC_DoubleLiteral. + def exitOC_DoubleLiteral(self, ctx:CypherParser.OC_DoubleLiteralContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ListLiteral. + def enterOC_ListLiteral(self, ctx:CypherParser.OC_ListLiteralContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ListLiteral. + def exitOC_ListLiteral(self, ctx:CypherParser.OC_ListLiteralContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_MapLiteral. + def enterOC_MapLiteral(self, ctx:CypherParser.OC_MapLiteralContext): + pass + + # Exit a parse tree produced by CypherParser#oC_MapLiteral. + def exitOC_MapLiteral(self, ctx:CypherParser.OC_MapLiteralContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_PropertyKeyName. + def enterOC_PropertyKeyName(self, ctx:CypherParser.OC_PropertyKeyNameContext): + pass + + # Exit a parse tree produced by CypherParser#oC_PropertyKeyName. + def exitOC_PropertyKeyName(self, ctx:CypherParser.OC_PropertyKeyNameContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Parameter. + def enterOC_Parameter(self, ctx:CypherParser.OC_ParameterContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Parameter. + def exitOC_Parameter(self, ctx:CypherParser.OC_ParameterContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_SchemaName. + def enterOC_SchemaName(self, ctx:CypherParser.OC_SchemaNameContext): + pass + + # Exit a parse tree produced by CypherParser#oC_SchemaName. + def exitOC_SchemaName(self, ctx:CypherParser.OC_SchemaNameContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_ReservedWord. + def enterOC_ReservedWord(self, ctx:CypherParser.OC_ReservedWordContext): + pass + + # Exit a parse tree produced by CypherParser#oC_ReservedWord. + def exitOC_ReservedWord(self, ctx:CypherParser.OC_ReservedWordContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_SymbolicName. + def enterOC_SymbolicName(self, ctx:CypherParser.OC_SymbolicNameContext): + pass + + # Exit a parse tree produced by CypherParser#oC_SymbolicName. + def exitOC_SymbolicName(self, ctx:CypherParser.OC_SymbolicNameContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_LeftArrowHead. + def enterOC_LeftArrowHead(self, ctx:CypherParser.OC_LeftArrowHeadContext): + pass + + # Exit a parse tree produced by CypherParser#oC_LeftArrowHead. + def exitOC_LeftArrowHead(self, ctx:CypherParser.OC_LeftArrowHeadContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_RightArrowHead. + def enterOC_RightArrowHead(self, ctx:CypherParser.OC_RightArrowHeadContext): + pass + + # Exit a parse tree produced by CypherParser#oC_RightArrowHead. + def exitOC_RightArrowHead(self, ctx:CypherParser.OC_RightArrowHeadContext): + pass + + + # Enter a parse tree produced by CypherParser#oC_Dash. + def enterOC_Dash(self, ctx:CypherParser.OC_DashContext): + pass + + # Exit a parse tree produced by CypherParser#oC_Dash. + def exitOC_Dash(self, ctx:CypherParser.OC_DashContext): + pass + + + +del CypherParser \ No newline at end of file diff --git a/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherParser.py b/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherParser.py new file mode 100644 index 000000000..168c61e20 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherParser.py @@ -0,0 +1,10440 @@ +# Generated from Cypher.g4 by ANTLR 4.13.2 +# encoding: utf-8 +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + +def serializedATN(): + return [ + 4,1,127,1575,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, + 7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7, + 13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2, + 20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7, + 26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31,2,32,7,32,2, + 33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,7,38,2,39,7, + 39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45,7,45,2, + 46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51,2,52,7, + 52,2,53,7,53,2,54,7,54,2,55,7,55,2,56,7,56,2,57,7,57,2,58,7,58,2, + 59,7,59,2,60,7,60,2,61,7,61,2,62,7,62,2,63,7,63,2,64,7,64,2,65,7, + 65,2,66,7,66,2,67,7,67,2,68,7,68,2,69,7,69,2,70,7,70,2,71,7,71,2, + 72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77,2,78,7, + 78,2,79,7,79,2,80,7,80,2,81,7,81,2,82,7,82,2,83,7,83,2,84,7,84,2, + 85,7,85,2,86,7,86,2,87,7,87,2,88,7,88,2,89,7,89,2,90,7,90,2,91,7, + 91,2,92,7,92,2,93,7,93,2,94,7,94,2,95,7,95,2,96,7,96,2,97,7,97,2, + 98,7,98,2,99,7,99,2,100,7,100,2,101,7,101,2,102,7,102,1,0,3,0,208, + 8,0,1,0,1,0,3,0,212,8,0,1,0,3,0,215,8,0,1,0,3,0,218,8,0,1,0,1,0, + 1,1,1,1,1,2,1,2,3,2,226,8,2,1,3,1,3,3,3,230,8,3,1,3,5,3,233,8,3, + 10,3,12,3,236,9,3,1,4,1,4,1,4,1,4,3,4,242,8,4,1,4,1,4,1,4,3,4,247, + 8,4,1,4,3,4,250,8,4,1,5,1,5,3,5,254,8,5,1,6,1,6,3,6,258,8,6,5,6, + 260,8,6,10,6,12,6,263,9,6,1,6,1,6,1,6,3,6,268,8,6,5,6,270,8,6,10, + 6,12,6,273,9,6,1,6,1,6,3,6,277,8,6,1,6,5,6,280,8,6,10,6,12,6,283, + 9,6,1,6,3,6,286,8,6,1,6,3,6,289,8,6,3,6,291,8,6,1,7,1,7,3,7,295, + 8,7,5,7,297,8,7,10,7,12,7,300,9,7,1,7,1,7,3,7,304,8,7,5,7,306,8, + 7,10,7,12,7,309,9,7,1,7,1,7,3,7,313,8,7,4,7,315,8,7,11,7,12,7,316, + 1,7,1,7,1,8,1,8,1,8,1,8,1,8,3,8,326,8,8,1,9,1,9,1,9,3,9,331,8,9, + 1,10,1,10,3,10,335,8,10,1,10,1,10,3,10,339,8,10,1,10,1,10,3,10,343, + 8,10,1,10,3,10,346,8,10,1,11,1,11,3,11,350,8,11,1,11,1,11,1,11,1, + 11,1,11,1,11,1,12,1,12,3,12,360,8,12,1,12,1,12,1,12,5,12,365,8,12, + 10,12,12,12,368,9,12,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13, + 1,13,3,13,380,8,13,1,14,1,14,3,14,384,8,14,1,14,1,14,1,15,1,15,3, + 15,390,8,15,1,15,1,15,3,15,394,8,15,1,15,1,15,3,15,398,8,15,1,15, + 5,15,401,8,15,10,15,12,15,404,9,15,1,16,1,16,3,16,408,8,16,1,16, + 1,16,3,16,412,8,16,1,16,1,16,1,16,1,16,3,16,418,8,16,1,16,1,16,3, + 16,422,8,16,1,16,1,16,1,16,1,16,3,16,428,8,16,1,16,1,16,3,16,432, + 8,16,1,16,1,16,1,16,1,16,3,16,438,8,16,1,16,1,16,3,16,442,8,16,1, + 17,1,17,3,17,446,8,17,1,17,1,17,3,17,450,8,17,1,17,1,17,3,17,454, + 8,17,1,17,1,17,3,17,458,8,17,1,17,5,17,461,8,17,10,17,12,17,464, + 9,17,1,18,1,18,1,18,1,18,3,18,470,8,18,1,18,1,18,3,18,474,8,18,1, + 18,5,18,477,8,18,10,18,12,18,480,9,18,1,19,1,19,1,19,1,19,3,19,486, + 8,19,1,20,1,20,1,20,1,20,3,20,492,8,20,1,20,1,20,1,20,3,20,497,8, + 20,1,21,1,21,1,21,1,21,3,21,503,8,21,1,21,3,21,506,8,21,1,21,1,21, + 1,21,1,21,3,21,512,8,21,3,21,514,8,21,1,22,1,22,3,22,518,8,22,1, + 22,1,22,3,22,522,8,22,1,22,5,22,525,8,22,10,22,12,22,528,9,22,1, + 22,3,22,531,8,22,1,22,3,22,534,8,22,1,23,1,23,1,23,1,23,1,23,3,23, + 541,8,23,1,23,1,23,1,24,1,24,1,24,3,24,548,8,24,1,24,3,24,551,8, + 24,1,25,1,25,1,25,1,26,3,26,557,8,26,1,26,3,26,560,8,26,1,26,1,26, + 1,26,1,26,3,26,566,8,26,1,26,1,26,3,26,570,8,26,1,26,1,26,3,26,574, + 8,26,1,27,1,27,3,27,578,8,27,1,27,1,27,3,27,582,8,27,1,27,5,27,585, + 8,27,10,27,12,27,588,9,27,1,27,1,27,3,27,592,8,27,1,27,1,27,3,27, + 596,8,27,1,27,5,27,599,8,27,10,27,12,27,602,9,27,3,27,604,8,27,1, + 28,1,28,1,28,1,28,1,28,1,28,1,28,3,28,613,8,28,1,29,1,29,1,29,1, + 29,1,29,1,29,1,29,3,29,622,8,29,1,29,5,29,625,8,29,10,29,12,29,628, + 9,29,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1,31,1,32,1,32,3,32,640, + 8,32,1,32,3,32,643,8,32,1,33,1,33,1,33,1,33,1,34,1,34,3,34,651,8, + 34,1,34,1,34,3,34,655,8,34,1,34,5,34,658,8,34,10,34,12,34,661,9, + 34,1,35,1,35,3,35,665,8,35,1,35,1,35,3,35,669,8,35,1,35,1,35,1,35, + 3,35,674,8,35,1,36,1,36,1,37,1,37,3,37,680,8,37,1,37,5,37,683,8, + 37,10,37,12,37,686,9,37,1,37,1,37,1,37,1,37,3,37,692,8,37,1,38,1, + 38,3,38,696,8,38,1,38,4,38,699,8,38,11,38,12,38,700,1,39,1,39,3, + 39,705,8,39,1,39,1,39,3,39,709,8,39,3,39,711,8,39,1,39,1,39,3,39, + 715,8,39,3,39,717,8,39,1,39,1,39,3,39,721,8,39,3,39,723,8,39,1,39, + 1,39,1,40,1,40,3,40,729,8,40,1,40,1,40,1,41,1,41,3,41,735,8,41,1, + 41,1,41,3,41,739,8,41,1,41,3,41,742,8,41,1,41,3,41,745,8,41,1,41, + 1,41,3,41,749,8,41,1,41,1,41,1,41,1,41,3,41,755,8,41,1,41,1,41,3, + 41,759,8,41,1,41,3,41,762,8,41,1,41,3,41,765,8,41,1,41,1,41,1,41, + 1,41,3,41,771,8,41,1,41,3,41,774,8,41,1,41,3,41,777,8,41,1,41,1, + 41,3,41,781,8,41,1,41,1,41,1,41,1,41,3,41,787,8,41,1,41,3,41,790, + 8,41,1,41,3,41,793,8,41,1,41,1,41,3,41,797,8,41,1,42,1,42,3,42,801, + 8,42,1,42,1,42,3,42,805,8,42,3,42,807,8,42,1,42,1,42,3,42,811,8, + 42,3,42,813,8,42,1,42,3,42,816,8,42,1,42,1,42,3,42,820,8,42,3,42, + 822,8,42,1,42,1,42,1,43,1,43,3,43,828,8,43,1,44,1,44,3,44,832,8, + 44,1,44,1,44,3,44,836,8,44,1,44,1,44,3,44,840,8,44,1,44,3,44,843, + 8,44,1,44,5,44,846,8,44,10,44,12,44,849,9,44,1,45,1,45,3,45,853, + 8,45,1,45,5,45,856,8,45,10,45,12,45,859,9,45,1,46,1,46,3,46,863, + 8,46,1,46,1,46,1,47,1,47,3,47,869,8,47,1,47,1,47,3,47,873,8,47,3, + 47,875,8,47,1,47,1,47,3,47,879,8,47,1,47,1,47,3,47,883,8,47,3,47, + 885,8,47,3,47,887,8,47,1,48,1,48,1,49,1,49,1,50,1,50,3,50,895,8, + 50,1,50,4,50,898,8,50,11,50,12,50,899,1,51,1,51,1,52,1,52,1,52,1, + 52,1,52,5,52,909,8,52,10,52,12,52,912,9,52,1,53,1,53,1,53,1,53,1, + 53,5,53,919,8,53,10,53,12,53,922,9,53,1,54,1,54,1,54,1,54,1,54,5, + 54,929,8,54,10,54,12,54,932,9,54,1,55,1,55,3,55,936,8,55,5,55,938, + 8,55,10,55,12,55,941,9,55,1,55,1,55,1,56,1,56,3,56,947,8,56,1,56, + 5,56,950,8,56,10,56,12,56,953,9,56,1,57,1,57,3,57,957,8,57,1,57, + 1,57,1,57,3,57,962,8,57,1,57,1,57,1,57,3,57,967,8,57,1,57,1,57,1, + 57,3,57,972,8,57,1,57,1,57,1,57,3,57,977,8,57,1,57,1,57,1,57,3,57, + 982,8,57,1,57,3,57,985,8,57,1,58,1,58,1,58,1,58,5,58,991,8,58,10, + 58,12,58,994,9,58,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1, + 59,3,59,1006,8,59,1,59,3,59,1009,8,59,1,59,1,59,1,60,1,60,1,60,3, + 60,1016,8,60,1,60,1,60,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1, + 61,1,61,3,61,1030,8,61,1,62,1,62,3,62,1034,8,62,1,62,1,62,3,62,1038, + 8,62,1,62,1,62,3,62,1042,8,62,1,62,1,62,3,62,1046,8,62,1,62,5,62, + 1049,8,62,10,62,12,62,1052,9,62,1,63,1,63,3,63,1056,8,63,1,63,1, + 63,3,63,1060,8,63,1,63,1,63,3,63,1064,8,63,1,63,1,63,3,63,1068,8, + 63,1,63,1,63,3,63,1072,8,63,1,63,1,63,3,63,1076,8,63,1,63,5,63,1079, + 8,63,10,63,12,63,1082,9,63,1,64,1,64,3,64,1086,8,64,1,64,1,64,3, + 64,1090,8,64,1,64,5,64,1093,8,64,10,64,12,64,1096,9,64,1,65,1,65, + 1,65,3,65,1101,8,65,1,65,3,65,1104,8,65,1,66,1,66,3,66,1108,8,66, + 1,66,1,66,3,66,1112,8,66,1,66,5,66,1115,8,66,10,66,12,66,1118,9, + 66,1,66,3,66,1121,8,66,1,66,3,66,1124,8,66,1,67,1,67,1,67,1,67,1, + 67,1,67,3,67,1132,8,67,1,67,1,67,3,67,1136,8,67,1,67,3,67,1139,8, + 67,1,68,1,68,3,68,1143,8,68,1,68,1,68,1,69,1,69,1,69,1,69,1,69,3, + 69,1152,8,69,1,69,1,69,3,69,1156,8,69,1,69,1,69,3,69,1160,8,69,1, + 69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,3,69,1171,8,69,1,70,1, + 70,3,70,1175,8,70,1,70,4,70,1178,8,70,11,70,12,70,1179,1,70,1,70, + 3,70,1184,8,70,1,70,1,70,3,70,1188,8,70,1,70,4,70,1191,8,70,11,70, + 12,70,1192,3,70,1195,8,70,1,70,3,70,1198,8,70,1,70,1,70,3,70,1202, + 8,70,1,70,3,70,1205,8,70,1,70,3,70,1208,8,70,1,70,1,70,1,71,1,71, + 3,71,1214,8,71,1,71,1,71,3,71,1218,8,71,1,71,1,71,3,71,1222,8,71, + 1,71,1,71,1,72,1,72,3,72,1228,8,72,1,72,1,72,3,72,1232,8,72,1,72, + 1,72,3,72,1236,8,72,1,72,3,72,1239,8,72,1,72,3,72,1242,8,72,1,72, + 1,72,1,73,1,73,3,73,1248,8,73,1,73,1,73,3,73,1252,8,73,1,73,1,73, + 3,73,1256,8,73,3,73,1258,8,73,1,73,1,73,3,73,1262,8,73,1,73,1,73, + 3,73,1266,8,73,3,73,1268,8,73,1,73,1,73,3,73,1272,8,73,1,73,1,73, + 3,73,1276,8,73,1,73,1,73,1,74,1,74,3,74,1282,8,74,1,74,1,74,3,74, + 1286,8,74,1,74,1,74,3,74,1290,8,74,1,74,1,74,1,74,1,74,3,74,1296, + 8,74,1,74,1,74,3,74,1300,8,74,1,74,1,74,3,74,1304,8,74,1,74,1,74, + 1,74,1,74,3,74,1310,8,74,1,74,1,74,3,74,1314,8,74,1,74,1,74,3,74, + 1318,8,74,1,74,1,74,1,74,1,74,3,74,1324,8,74,1,74,1,74,3,74,1328, + 8,74,1,74,1,74,3,74,1332,8,74,1,74,1,74,3,74,1336,8,74,1,75,1,75, + 3,75,1340,8,75,1,75,3,75,1343,8,75,1,76,1,76,1,77,1,77,3,77,1349, + 8,77,1,77,1,77,3,77,1353,8,77,1,77,1,77,1,78,1,78,1,78,1,78,1,78, + 1,78,1,79,1,79,3,79,1365,8,79,1,79,1,79,3,79,1369,8,79,1,79,1,79, + 3,79,1373,8,79,3,79,1375,8,79,1,79,1,79,3,79,1379,8,79,1,79,1,79, + 3,79,1383,8,79,1,79,1,79,3,79,1387,8,79,5,79,1389,8,79,10,79,12, + 79,1392,9,79,3,79,1394,8,79,1,79,1,79,1,80,1,80,1,80,1,81,1,81,3, + 81,1403,8,81,1,81,1,81,3,81,1407,8,81,1,81,1,81,1,81,3,81,1412,8, + 81,1,81,3,81,1415,8,81,3,81,1417,8,81,1,81,3,81,1420,8,81,1,81,1, + 81,1,82,1,82,3,82,1426,8,82,1,82,1,82,3,82,1430,8,82,1,82,1,82,3, + 82,1434,8,82,1,82,1,82,3,82,1438,8,82,1,82,1,82,3,82,1442,8,82,5, + 82,1444,8,82,10,82,12,82,1447,9,82,3,82,1449,8,82,1,82,1,82,1,83, + 1,83,1,84,1,84,1,85,1,85,1,85,1,86,1,86,1,86,5,86,1463,8,86,10,86, + 12,86,1466,9,86,1,87,1,87,1,88,1,88,1,88,1,88,1,88,1,88,3,88,1476, + 8,88,1,89,1,89,1,90,1,90,3,90,1482,8,90,1,91,1,91,1,92,1,92,1,93, + 1,93,3,93,1490,8,93,1,93,1,93,3,93,1494,8,93,1,93,1,93,3,93,1498, + 8,93,1,93,1,93,3,93,1502,8,93,5,93,1504,8,93,10,93,12,93,1507,9, + 93,3,93,1509,8,93,1,93,1,93,1,94,1,94,3,94,1515,8,94,1,94,1,94,3, + 94,1519,8,94,1,94,1,94,3,94,1523,8,94,1,94,1,94,3,94,1527,8,94,1, + 94,1,94,3,94,1531,8,94,1,94,1,94,3,94,1535,8,94,1,94,1,94,3,94,1539, + 8,94,1,94,1,94,3,94,1543,8,94,5,94,1545,8,94,10,94,12,94,1548,9, + 94,3,94,1550,8,94,1,94,1,94,1,95,1,95,1,96,1,96,1,96,3,96,1559,8, + 96,1,97,1,97,3,97,1563,8,97,1,98,1,98,1,99,1,99,1,100,1,100,1,101, + 1,101,1,102,1,102,1,102,0,0,103,0,2,4,6,8,10,12,14,16,18,20,22,24, + 26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68, + 70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108, + 110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140, + 142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172, + 174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204, + 0,10,1,0,68,71,1,0,18,19,1,0,93,94,1,0,95,97,1,0,105,106,5,0,46, + 58,61,82,84,88,92,94,109,118,5,0,83,83,89,91,98,98,119,121,124,124, + 2,0,14,14,27,30,2,0,15,15,31,34,2,0,19,19,35,45,1793,0,207,1,0,0, + 0,2,221,1,0,0,0,4,225,1,0,0,0,6,227,1,0,0,0,8,249,1,0,0,0,10,253, + 1,0,0,0,12,290,1,0,0,0,14,314,1,0,0,0,16,325,1,0,0,0,18,330,1,0, + 0,0,20,334,1,0,0,0,22,347,1,0,0,0,24,357,1,0,0,0,26,379,1,0,0,0, + 28,381,1,0,0,0,30,387,1,0,0,0,32,441,1,0,0,0,34,445,1,0,0,0,36,465, + 1,0,0,0,38,485,1,0,0,0,40,487,1,0,0,0,42,498,1,0,0,0,44,515,1,0, + 0,0,46,540,1,0,0,0,48,544,1,0,0,0,50,552,1,0,0,0,52,559,1,0,0,0, + 54,603,1,0,0,0,56,612,1,0,0,0,58,614,1,0,0,0,60,629,1,0,0,0,62,633, + 1,0,0,0,64,637,1,0,0,0,66,644,1,0,0,0,68,648,1,0,0,0,70,673,1,0, + 0,0,72,675,1,0,0,0,74,691,1,0,0,0,76,693,1,0,0,0,78,702,1,0,0,0, + 80,726,1,0,0,0,82,796,1,0,0,0,84,798,1,0,0,0,86,827,1,0,0,0,88,829, + 1,0,0,0,90,850,1,0,0,0,92,860,1,0,0,0,94,866,1,0,0,0,96,888,1,0, + 0,0,98,890,1,0,0,0,100,892,1,0,0,0,102,901,1,0,0,0,104,903,1,0,0, + 0,106,913,1,0,0,0,108,923,1,0,0,0,110,939,1,0,0,0,112,944,1,0,0, + 0,114,984,1,0,0,0,116,986,1,0,0,0,118,1005,1,0,0,0,120,1012,1,0, + 0,0,122,1029,1,0,0,0,124,1031,1,0,0,0,126,1053,1,0,0,0,128,1083, + 1,0,0,0,130,1103,1,0,0,0,132,1105,1,0,0,0,134,1138,1,0,0,0,136,1140, + 1,0,0,0,138,1170,1,0,0,0,140,1194,1,0,0,0,142,1211,1,0,0,0,144,1225, + 1,0,0,0,146,1245,1,0,0,0,148,1335,1,0,0,0,150,1337,1,0,0,0,152,1344, + 1,0,0,0,154,1346,1,0,0,0,156,1356,1,0,0,0,158,1362,1,0,0,0,160,1397, + 1,0,0,0,162,1400,1,0,0,0,164,1423,1,0,0,0,166,1452,1,0,0,0,168,1454, + 1,0,0,0,170,1456,1,0,0,0,172,1464,1,0,0,0,174,1467,1,0,0,0,176,1475, + 1,0,0,0,178,1477,1,0,0,0,180,1481,1,0,0,0,182,1483,1,0,0,0,184,1485, + 1,0,0,0,186,1487,1,0,0,0,188,1512,1,0,0,0,190,1553,1,0,0,0,192,1555, + 1,0,0,0,194,1562,1,0,0,0,196,1564,1,0,0,0,198,1566,1,0,0,0,200,1568, + 1,0,0,0,202,1570,1,0,0,0,204,1572,1,0,0,0,206,208,5,125,0,0,207, + 206,1,0,0,0,207,208,1,0,0,0,208,209,1,0,0,0,209,214,3,2,1,0,210, + 212,5,125,0,0,211,210,1,0,0,0,211,212,1,0,0,0,212,213,1,0,0,0,213, + 215,5,1,0,0,214,211,1,0,0,0,214,215,1,0,0,0,215,217,1,0,0,0,216, + 218,5,125,0,0,217,216,1,0,0,0,217,218,1,0,0,0,218,219,1,0,0,0,219, + 220,5,0,0,1,220,1,1,0,0,0,221,222,3,4,2,0,222,3,1,0,0,0,223,226, + 3,6,3,0,224,226,3,42,21,0,225,223,1,0,0,0,225,224,1,0,0,0,226,5, + 1,0,0,0,227,234,3,10,5,0,228,230,5,125,0,0,229,228,1,0,0,0,229,230, + 1,0,0,0,230,231,1,0,0,0,231,233,3,8,4,0,232,229,1,0,0,0,233,236, + 1,0,0,0,234,232,1,0,0,0,234,235,1,0,0,0,235,7,1,0,0,0,236,234,1, + 0,0,0,237,238,5,46,0,0,238,239,5,125,0,0,239,241,5,47,0,0,240,242, + 5,125,0,0,241,240,1,0,0,0,241,242,1,0,0,0,242,243,1,0,0,0,243,250, + 3,10,5,0,244,246,5,46,0,0,245,247,5,125,0,0,246,245,1,0,0,0,246, + 247,1,0,0,0,247,248,1,0,0,0,248,250,3,10,5,0,249,237,1,0,0,0,249, + 244,1,0,0,0,250,9,1,0,0,0,251,254,3,12,6,0,252,254,3,14,7,0,253, + 251,1,0,0,0,253,252,1,0,0,0,254,11,1,0,0,0,255,257,3,18,9,0,256, + 258,5,125,0,0,257,256,1,0,0,0,257,258,1,0,0,0,258,260,1,0,0,0,259, + 255,1,0,0,0,260,263,1,0,0,0,261,259,1,0,0,0,261,262,1,0,0,0,262, + 264,1,0,0,0,263,261,1,0,0,0,264,291,3,50,25,0,265,267,3,18,9,0,266, + 268,5,125,0,0,267,266,1,0,0,0,267,268,1,0,0,0,268,270,1,0,0,0,269, + 265,1,0,0,0,270,273,1,0,0,0,271,269,1,0,0,0,271,272,1,0,0,0,272, + 274,1,0,0,0,273,271,1,0,0,0,274,281,3,16,8,0,275,277,5,125,0,0,276, + 275,1,0,0,0,276,277,1,0,0,0,277,278,1,0,0,0,278,280,3,16,8,0,279, + 276,1,0,0,0,280,283,1,0,0,0,281,279,1,0,0,0,281,282,1,0,0,0,282, + 288,1,0,0,0,283,281,1,0,0,0,284,286,5,125,0,0,285,284,1,0,0,0,285, + 286,1,0,0,0,286,287,1,0,0,0,287,289,3,50,25,0,288,285,1,0,0,0,288, + 289,1,0,0,0,289,291,1,0,0,0,290,261,1,0,0,0,290,271,1,0,0,0,291, + 13,1,0,0,0,292,294,3,18,9,0,293,295,5,125,0,0,294,293,1,0,0,0,294, + 295,1,0,0,0,295,297,1,0,0,0,296,292,1,0,0,0,297,300,1,0,0,0,298, + 296,1,0,0,0,298,299,1,0,0,0,299,307,1,0,0,0,300,298,1,0,0,0,301, + 303,3,16,8,0,302,304,5,125,0,0,303,302,1,0,0,0,303,304,1,0,0,0,304, + 306,1,0,0,0,305,301,1,0,0,0,306,309,1,0,0,0,307,305,1,0,0,0,307, + 308,1,0,0,0,308,310,1,0,0,0,309,307,1,0,0,0,310,312,3,48,24,0,311, + 313,5,125,0,0,312,311,1,0,0,0,312,313,1,0,0,0,313,315,1,0,0,0,314, + 298,1,0,0,0,315,316,1,0,0,0,316,314,1,0,0,0,316,317,1,0,0,0,317, + 318,1,0,0,0,318,319,3,12,6,0,319,15,1,0,0,0,320,326,3,28,14,0,321, + 326,3,24,12,0,322,326,3,34,17,0,323,326,3,30,15,0,324,326,3,36,18, + 0,325,320,1,0,0,0,325,321,1,0,0,0,325,322,1,0,0,0,325,323,1,0,0, + 0,325,324,1,0,0,0,326,17,1,0,0,0,327,331,3,20,10,0,328,331,3,22, + 11,0,329,331,3,40,20,0,330,327,1,0,0,0,330,328,1,0,0,0,330,329,1, + 0,0,0,331,19,1,0,0,0,332,333,5,48,0,0,333,335,5,125,0,0,334,332, + 1,0,0,0,334,335,1,0,0,0,335,336,1,0,0,0,336,338,5,49,0,0,337,339, + 5,125,0,0,338,337,1,0,0,0,338,339,1,0,0,0,339,340,1,0,0,0,340,345, + 3,68,34,0,341,343,5,125,0,0,342,341,1,0,0,0,342,343,1,0,0,0,343, + 344,1,0,0,0,344,346,3,66,33,0,345,342,1,0,0,0,345,346,1,0,0,0,346, + 21,1,0,0,0,347,349,5,50,0,0,348,350,5,125,0,0,349,348,1,0,0,0,349, + 350,1,0,0,0,350,351,1,0,0,0,351,352,3,102,51,0,352,353,5,125,0,0, + 353,354,5,51,0,0,354,355,5,125,0,0,355,356,3,174,87,0,356,23,1,0, + 0,0,357,359,5,52,0,0,358,360,5,125,0,0,359,358,1,0,0,0,359,360,1, + 0,0,0,360,361,1,0,0,0,361,366,3,70,35,0,362,363,5,125,0,0,363,365, + 3,26,13,0,364,362,1,0,0,0,365,368,1,0,0,0,366,364,1,0,0,0,366,367, + 1,0,0,0,367,25,1,0,0,0,368,366,1,0,0,0,369,370,5,53,0,0,370,371, + 5,125,0,0,371,372,5,49,0,0,372,373,5,125,0,0,373,380,3,30,15,0,374, + 375,5,53,0,0,375,376,5,125,0,0,376,377,5,54,0,0,377,378,5,125,0, + 0,378,380,3,30,15,0,379,369,1,0,0,0,379,374,1,0,0,0,380,27,1,0,0, + 0,381,383,5,54,0,0,382,384,5,125,0,0,383,382,1,0,0,0,383,384,1,0, + 0,0,384,385,1,0,0,0,385,386,3,68,34,0,386,29,1,0,0,0,387,389,5,55, + 0,0,388,390,5,125,0,0,389,388,1,0,0,0,389,390,1,0,0,0,390,391,1, + 0,0,0,391,402,3,32,16,0,392,394,5,125,0,0,393,392,1,0,0,0,393,394, + 1,0,0,0,394,395,1,0,0,0,395,397,5,2,0,0,396,398,5,125,0,0,397,396, + 1,0,0,0,397,398,1,0,0,0,398,399,1,0,0,0,399,401,3,32,16,0,400,393, + 1,0,0,0,401,404,1,0,0,0,402,400,1,0,0,0,402,403,1,0,0,0,403,31,1, + 0,0,0,404,402,1,0,0,0,405,407,3,100,50,0,406,408,5,125,0,0,407,406, + 1,0,0,0,407,408,1,0,0,0,408,409,1,0,0,0,409,411,5,3,0,0,410,412, + 5,125,0,0,411,410,1,0,0,0,411,412,1,0,0,0,412,413,1,0,0,0,413,414, + 3,102,51,0,414,442,1,0,0,0,415,417,3,174,87,0,416,418,5,125,0,0, + 417,416,1,0,0,0,417,418,1,0,0,0,418,419,1,0,0,0,419,421,5,3,0,0, + 420,422,5,125,0,0,421,420,1,0,0,0,421,422,1,0,0,0,422,423,1,0,0, + 0,423,424,3,102,51,0,424,442,1,0,0,0,425,427,3,174,87,0,426,428, + 5,125,0,0,427,426,1,0,0,0,427,428,1,0,0,0,428,429,1,0,0,0,429,431, + 5,4,0,0,430,432,5,125,0,0,431,430,1,0,0,0,431,432,1,0,0,0,432,433, + 1,0,0,0,433,434,3,102,51,0,434,442,1,0,0,0,435,437,3,174,87,0,436, + 438,5,125,0,0,437,436,1,0,0,0,437,438,1,0,0,0,438,439,1,0,0,0,439, + 440,3,90,45,0,440,442,1,0,0,0,441,405,1,0,0,0,441,415,1,0,0,0,441, + 425,1,0,0,0,441,435,1,0,0,0,442,33,1,0,0,0,443,444,5,56,0,0,444, + 446,5,125,0,0,445,443,1,0,0,0,445,446,1,0,0,0,446,447,1,0,0,0,447, + 449,5,57,0,0,448,450,5,125,0,0,449,448,1,0,0,0,449,450,1,0,0,0,450, + 451,1,0,0,0,451,462,3,102,51,0,452,454,5,125,0,0,453,452,1,0,0,0, + 453,454,1,0,0,0,454,455,1,0,0,0,455,457,5,2,0,0,456,458,5,125,0, + 0,457,456,1,0,0,0,457,458,1,0,0,0,458,459,1,0,0,0,459,461,3,102, + 51,0,460,453,1,0,0,0,461,464,1,0,0,0,462,460,1,0,0,0,462,463,1,0, + 0,0,463,35,1,0,0,0,464,462,1,0,0,0,465,466,5,58,0,0,466,467,5,125, + 0,0,467,478,3,38,19,0,468,470,5,125,0,0,469,468,1,0,0,0,469,470, + 1,0,0,0,470,471,1,0,0,0,471,473,5,2,0,0,472,474,5,125,0,0,473,472, + 1,0,0,0,473,474,1,0,0,0,474,475,1,0,0,0,475,477,3,38,19,0,476,469, + 1,0,0,0,477,480,1,0,0,0,478,476,1,0,0,0,478,479,1,0,0,0,479,37,1, + 0,0,0,480,478,1,0,0,0,481,482,3,174,87,0,482,483,3,90,45,0,483,486, + 1,0,0,0,484,486,3,100,50,0,485,481,1,0,0,0,485,484,1,0,0,0,486,39, + 1,0,0,0,487,488,5,59,0,0,488,489,5,125,0,0,489,496,3,164,82,0,490, + 492,5,125,0,0,491,490,1,0,0,0,491,492,1,0,0,0,492,493,1,0,0,0,493, + 494,5,60,0,0,494,495,5,125,0,0,495,497,3,44,22,0,496,491,1,0,0,0, + 496,497,1,0,0,0,497,41,1,0,0,0,498,499,5,59,0,0,499,502,5,125,0, + 0,500,503,3,164,82,0,501,503,3,166,83,0,502,500,1,0,0,0,502,501, + 1,0,0,0,503,513,1,0,0,0,504,506,5,125,0,0,505,504,1,0,0,0,505,506, + 1,0,0,0,506,507,1,0,0,0,507,508,5,60,0,0,508,511,5,125,0,0,509,512, + 5,5,0,0,510,512,3,44,22,0,511,509,1,0,0,0,511,510,1,0,0,0,512,514, + 1,0,0,0,513,505,1,0,0,0,513,514,1,0,0,0,514,43,1,0,0,0,515,526,3, + 46,23,0,516,518,5,125,0,0,517,516,1,0,0,0,517,518,1,0,0,0,518,519, + 1,0,0,0,519,521,5,2,0,0,520,522,5,125,0,0,521,520,1,0,0,0,521,522, + 1,0,0,0,522,523,1,0,0,0,523,525,3,46,23,0,524,517,1,0,0,0,525,528, + 1,0,0,0,526,524,1,0,0,0,526,527,1,0,0,0,527,533,1,0,0,0,528,526, + 1,0,0,0,529,531,5,125,0,0,530,529,1,0,0,0,530,531,1,0,0,0,531,532, + 1,0,0,0,532,534,3,66,33,0,533,530,1,0,0,0,533,534,1,0,0,0,534,45, + 1,0,0,0,535,536,3,168,84,0,536,537,5,125,0,0,537,538,5,51,0,0,538, + 539,5,125,0,0,539,541,1,0,0,0,540,535,1,0,0,0,540,541,1,0,0,0,541, + 542,1,0,0,0,542,543,3,174,87,0,543,47,1,0,0,0,544,545,5,61,0,0,545, + 550,3,52,26,0,546,548,5,125,0,0,547,546,1,0,0,0,547,548,1,0,0,0, + 548,549,1,0,0,0,549,551,3,66,33,0,550,547,1,0,0,0,550,551,1,0,0, + 0,551,49,1,0,0,0,552,553,5,62,0,0,553,554,3,52,26,0,554,51,1,0,0, + 0,555,557,5,125,0,0,556,555,1,0,0,0,556,557,1,0,0,0,557,558,1,0, + 0,0,558,560,5,63,0,0,559,556,1,0,0,0,559,560,1,0,0,0,560,561,1,0, + 0,0,561,562,5,125,0,0,562,565,3,54,27,0,563,564,5,125,0,0,564,566, + 3,58,29,0,565,563,1,0,0,0,565,566,1,0,0,0,566,569,1,0,0,0,567,568, + 5,125,0,0,568,570,3,60,30,0,569,567,1,0,0,0,569,570,1,0,0,0,570, + 573,1,0,0,0,571,572,5,125,0,0,572,574,3,62,31,0,573,571,1,0,0,0, + 573,574,1,0,0,0,574,53,1,0,0,0,575,586,5,5,0,0,576,578,5,125,0,0, + 577,576,1,0,0,0,577,578,1,0,0,0,578,579,1,0,0,0,579,581,5,2,0,0, + 580,582,5,125,0,0,581,580,1,0,0,0,581,582,1,0,0,0,582,583,1,0,0, + 0,583,585,3,56,28,0,584,577,1,0,0,0,585,588,1,0,0,0,586,584,1,0, + 0,0,586,587,1,0,0,0,587,604,1,0,0,0,588,586,1,0,0,0,589,600,3,56, + 28,0,590,592,5,125,0,0,591,590,1,0,0,0,591,592,1,0,0,0,592,593,1, + 0,0,0,593,595,5,2,0,0,594,596,5,125,0,0,595,594,1,0,0,0,595,596, + 1,0,0,0,596,597,1,0,0,0,597,599,3,56,28,0,598,591,1,0,0,0,599,602, + 1,0,0,0,600,598,1,0,0,0,600,601,1,0,0,0,601,604,1,0,0,0,602,600, + 1,0,0,0,603,575,1,0,0,0,603,589,1,0,0,0,604,55,1,0,0,0,605,606,3, + 102,51,0,606,607,5,125,0,0,607,608,5,51,0,0,608,609,5,125,0,0,609, + 610,3,174,87,0,610,613,1,0,0,0,611,613,3,102,51,0,612,605,1,0,0, + 0,612,611,1,0,0,0,613,57,1,0,0,0,614,615,5,64,0,0,615,616,5,125, + 0,0,616,617,5,65,0,0,617,618,5,125,0,0,618,626,3,64,32,0,619,621, + 5,2,0,0,620,622,5,125,0,0,621,620,1,0,0,0,621,622,1,0,0,0,622,623, + 1,0,0,0,623,625,3,64,32,0,624,619,1,0,0,0,625,628,1,0,0,0,626,624, + 1,0,0,0,626,627,1,0,0,0,627,59,1,0,0,0,628,626,1,0,0,0,629,630,5, + 66,0,0,630,631,5,125,0,0,631,632,3,102,51,0,632,61,1,0,0,0,633,634, + 5,67,0,0,634,635,5,125,0,0,635,636,3,102,51,0,636,63,1,0,0,0,637, + 642,3,102,51,0,638,640,5,125,0,0,639,638,1,0,0,0,639,640,1,0,0,0, + 640,641,1,0,0,0,641,643,7,0,0,0,642,639,1,0,0,0,642,643,1,0,0,0, + 643,65,1,0,0,0,644,645,5,72,0,0,645,646,5,125,0,0,646,647,3,102, + 51,0,647,67,1,0,0,0,648,659,3,70,35,0,649,651,5,125,0,0,650,649, + 1,0,0,0,650,651,1,0,0,0,651,652,1,0,0,0,652,654,5,2,0,0,653,655, + 5,125,0,0,654,653,1,0,0,0,654,655,1,0,0,0,655,656,1,0,0,0,656,658, + 3,70,35,0,657,650,1,0,0,0,658,661,1,0,0,0,659,657,1,0,0,0,659,660, + 1,0,0,0,660,69,1,0,0,0,661,659,1,0,0,0,662,664,3,174,87,0,663,665, + 5,125,0,0,664,663,1,0,0,0,664,665,1,0,0,0,665,666,1,0,0,0,666,668, + 5,3,0,0,667,669,5,125,0,0,668,667,1,0,0,0,668,669,1,0,0,0,669,670, + 1,0,0,0,670,671,3,72,36,0,671,674,1,0,0,0,672,674,3,72,36,0,673, + 662,1,0,0,0,673,672,1,0,0,0,674,71,1,0,0,0,675,676,3,74,37,0,676, + 73,1,0,0,0,677,684,3,78,39,0,678,680,5,125,0,0,679,678,1,0,0,0,679, + 680,1,0,0,0,680,681,1,0,0,0,681,683,3,80,40,0,682,679,1,0,0,0,683, + 686,1,0,0,0,684,682,1,0,0,0,684,685,1,0,0,0,685,692,1,0,0,0,686, + 684,1,0,0,0,687,688,5,6,0,0,688,689,3,74,37,0,689,690,5,7,0,0,690, + 692,1,0,0,0,691,677,1,0,0,0,691,687,1,0,0,0,692,75,1,0,0,0,693,698, + 3,78,39,0,694,696,5,125,0,0,695,694,1,0,0,0,695,696,1,0,0,0,696, + 697,1,0,0,0,697,699,3,80,40,0,698,695,1,0,0,0,699,700,1,0,0,0,700, + 698,1,0,0,0,700,701,1,0,0,0,701,77,1,0,0,0,702,704,5,6,0,0,703,705, + 5,125,0,0,704,703,1,0,0,0,704,705,1,0,0,0,705,710,1,0,0,0,706,708, + 3,174,87,0,707,709,5,125,0,0,708,707,1,0,0,0,708,709,1,0,0,0,709, + 711,1,0,0,0,710,706,1,0,0,0,710,711,1,0,0,0,711,716,1,0,0,0,712, + 714,3,90,45,0,713,715,5,125,0,0,714,713,1,0,0,0,714,715,1,0,0,0, + 715,717,1,0,0,0,716,712,1,0,0,0,716,717,1,0,0,0,717,722,1,0,0,0, + 718,720,3,86,43,0,719,721,5,125,0,0,720,719,1,0,0,0,720,721,1,0, + 0,0,721,723,1,0,0,0,722,718,1,0,0,0,722,723,1,0,0,0,723,724,1,0, + 0,0,724,725,5,7,0,0,725,79,1,0,0,0,726,728,3,82,41,0,727,729,5,125, + 0,0,728,727,1,0,0,0,728,729,1,0,0,0,729,730,1,0,0,0,730,731,3,78, + 39,0,731,81,1,0,0,0,732,734,3,200,100,0,733,735,5,125,0,0,734,733, + 1,0,0,0,734,735,1,0,0,0,735,736,1,0,0,0,736,738,3,204,102,0,737, + 739,5,125,0,0,738,737,1,0,0,0,738,739,1,0,0,0,739,741,1,0,0,0,740, + 742,3,84,42,0,741,740,1,0,0,0,741,742,1,0,0,0,742,744,1,0,0,0,743, + 745,5,125,0,0,744,743,1,0,0,0,744,745,1,0,0,0,745,746,1,0,0,0,746, + 748,3,204,102,0,747,749,5,125,0,0,748,747,1,0,0,0,748,749,1,0,0, + 0,749,750,1,0,0,0,750,751,3,202,101,0,751,797,1,0,0,0,752,754,3, + 200,100,0,753,755,5,125,0,0,754,753,1,0,0,0,754,755,1,0,0,0,755, + 756,1,0,0,0,756,758,3,204,102,0,757,759,5,125,0,0,758,757,1,0,0, + 0,758,759,1,0,0,0,759,761,1,0,0,0,760,762,3,84,42,0,761,760,1,0, + 0,0,761,762,1,0,0,0,762,764,1,0,0,0,763,765,5,125,0,0,764,763,1, + 0,0,0,764,765,1,0,0,0,765,766,1,0,0,0,766,767,3,204,102,0,767,797, + 1,0,0,0,768,770,3,204,102,0,769,771,5,125,0,0,770,769,1,0,0,0,770, + 771,1,0,0,0,771,773,1,0,0,0,772,774,3,84,42,0,773,772,1,0,0,0,773, + 774,1,0,0,0,774,776,1,0,0,0,775,777,5,125,0,0,776,775,1,0,0,0,776, + 777,1,0,0,0,777,778,1,0,0,0,778,780,3,204,102,0,779,781,5,125,0, + 0,780,779,1,0,0,0,780,781,1,0,0,0,781,782,1,0,0,0,782,783,3,202, + 101,0,783,797,1,0,0,0,784,786,3,204,102,0,785,787,5,125,0,0,786, + 785,1,0,0,0,786,787,1,0,0,0,787,789,1,0,0,0,788,790,3,84,42,0,789, + 788,1,0,0,0,789,790,1,0,0,0,790,792,1,0,0,0,791,793,5,125,0,0,792, + 791,1,0,0,0,792,793,1,0,0,0,793,794,1,0,0,0,794,795,3,204,102,0, + 795,797,1,0,0,0,796,732,1,0,0,0,796,752,1,0,0,0,796,768,1,0,0,0, + 796,784,1,0,0,0,797,83,1,0,0,0,798,800,5,8,0,0,799,801,5,125,0,0, + 800,799,1,0,0,0,800,801,1,0,0,0,801,806,1,0,0,0,802,804,3,174,87, + 0,803,805,5,125,0,0,804,803,1,0,0,0,804,805,1,0,0,0,805,807,1,0, + 0,0,806,802,1,0,0,0,806,807,1,0,0,0,807,812,1,0,0,0,808,810,3,88, + 44,0,809,811,5,125,0,0,810,809,1,0,0,0,810,811,1,0,0,0,811,813,1, + 0,0,0,812,808,1,0,0,0,812,813,1,0,0,0,813,815,1,0,0,0,814,816,3, + 94,47,0,815,814,1,0,0,0,815,816,1,0,0,0,816,821,1,0,0,0,817,819, + 3,86,43,0,818,820,5,125,0,0,819,818,1,0,0,0,819,820,1,0,0,0,820, + 822,1,0,0,0,821,817,1,0,0,0,821,822,1,0,0,0,822,823,1,0,0,0,823, + 824,5,9,0,0,824,85,1,0,0,0,825,828,3,188,94,0,826,828,3,192,96,0, + 827,825,1,0,0,0,827,826,1,0,0,0,828,87,1,0,0,0,829,831,5,10,0,0, + 830,832,5,125,0,0,831,830,1,0,0,0,831,832,1,0,0,0,832,833,1,0,0, + 0,833,847,3,98,49,0,834,836,5,125,0,0,835,834,1,0,0,0,835,836,1, + 0,0,0,836,837,1,0,0,0,837,839,5,11,0,0,838,840,5,10,0,0,839,838, + 1,0,0,0,839,840,1,0,0,0,840,842,1,0,0,0,841,843,5,125,0,0,842,841, + 1,0,0,0,842,843,1,0,0,0,843,844,1,0,0,0,844,846,3,98,49,0,845,835, + 1,0,0,0,846,849,1,0,0,0,847,845,1,0,0,0,847,848,1,0,0,0,848,89,1, + 0,0,0,849,847,1,0,0,0,850,857,3,92,46,0,851,853,5,125,0,0,852,851, + 1,0,0,0,852,853,1,0,0,0,853,854,1,0,0,0,854,856,3,92,46,0,855,852, + 1,0,0,0,856,859,1,0,0,0,857,855,1,0,0,0,857,858,1,0,0,0,858,91,1, + 0,0,0,859,857,1,0,0,0,860,862,5,10,0,0,861,863,5,125,0,0,862,861, + 1,0,0,0,862,863,1,0,0,0,863,864,1,0,0,0,864,865,3,96,48,0,865,93, + 1,0,0,0,866,868,5,5,0,0,867,869,5,125,0,0,868,867,1,0,0,0,868,869, + 1,0,0,0,869,874,1,0,0,0,870,872,3,182,91,0,871,873,5,125,0,0,872, + 871,1,0,0,0,872,873,1,0,0,0,873,875,1,0,0,0,874,870,1,0,0,0,874, + 875,1,0,0,0,875,886,1,0,0,0,876,878,5,12,0,0,877,879,5,125,0,0,878, + 877,1,0,0,0,878,879,1,0,0,0,879,884,1,0,0,0,880,882,3,182,91,0,881, + 883,5,125,0,0,882,881,1,0,0,0,882,883,1,0,0,0,883,885,1,0,0,0,884, + 880,1,0,0,0,884,885,1,0,0,0,885,887,1,0,0,0,886,876,1,0,0,0,886, + 887,1,0,0,0,887,95,1,0,0,0,888,889,3,194,97,0,889,97,1,0,0,0,890, + 891,3,194,97,0,891,99,1,0,0,0,892,897,3,138,69,0,893,895,5,125,0, + 0,894,893,1,0,0,0,894,895,1,0,0,0,895,896,1,0,0,0,896,898,3,136, + 68,0,897,894,1,0,0,0,898,899,1,0,0,0,899,897,1,0,0,0,899,900,1,0, + 0,0,900,101,1,0,0,0,901,902,3,104,52,0,902,103,1,0,0,0,903,910,3, + 106,53,0,904,905,5,125,0,0,905,906,5,73,0,0,906,907,5,125,0,0,907, + 909,3,106,53,0,908,904,1,0,0,0,909,912,1,0,0,0,910,908,1,0,0,0,910, + 911,1,0,0,0,911,105,1,0,0,0,912,910,1,0,0,0,913,920,3,108,54,0,914, + 915,5,125,0,0,915,916,5,74,0,0,916,917,5,125,0,0,917,919,3,108,54, + 0,918,914,1,0,0,0,919,922,1,0,0,0,920,918,1,0,0,0,920,921,1,0,0, + 0,921,107,1,0,0,0,922,920,1,0,0,0,923,930,3,110,55,0,924,925,5,125, + 0,0,925,926,5,75,0,0,926,927,5,125,0,0,927,929,3,110,55,0,928,924, + 1,0,0,0,929,932,1,0,0,0,930,928,1,0,0,0,930,931,1,0,0,0,931,109, + 1,0,0,0,932,930,1,0,0,0,933,935,5,76,0,0,934,936,5,125,0,0,935,934, + 1,0,0,0,935,936,1,0,0,0,936,938,1,0,0,0,937,933,1,0,0,0,938,941, + 1,0,0,0,939,937,1,0,0,0,939,940,1,0,0,0,940,942,1,0,0,0,941,939, + 1,0,0,0,942,943,3,112,56,0,943,111,1,0,0,0,944,951,3,116,58,0,945, + 947,5,125,0,0,946,945,1,0,0,0,946,947,1,0,0,0,947,948,1,0,0,0,948, + 950,3,114,57,0,949,946,1,0,0,0,950,953,1,0,0,0,951,949,1,0,0,0,951, + 952,1,0,0,0,952,113,1,0,0,0,953,951,1,0,0,0,954,956,5,3,0,0,955, + 957,5,125,0,0,956,955,1,0,0,0,956,957,1,0,0,0,957,958,1,0,0,0,958, + 985,3,116,58,0,959,961,5,13,0,0,960,962,5,125,0,0,961,960,1,0,0, + 0,961,962,1,0,0,0,962,963,1,0,0,0,963,985,3,116,58,0,964,966,5,14, + 0,0,965,967,5,125,0,0,966,965,1,0,0,0,966,967,1,0,0,0,967,968,1, + 0,0,0,968,985,3,116,58,0,969,971,5,15,0,0,970,972,5,125,0,0,971, + 970,1,0,0,0,971,972,1,0,0,0,972,973,1,0,0,0,973,985,3,116,58,0,974, + 976,5,16,0,0,975,977,5,125,0,0,976,975,1,0,0,0,976,977,1,0,0,0,977, + 978,1,0,0,0,978,985,3,116,58,0,979,981,5,17,0,0,980,982,5,125,0, + 0,981,980,1,0,0,0,981,982,1,0,0,0,982,983,1,0,0,0,983,985,3,116, + 58,0,984,954,1,0,0,0,984,959,1,0,0,0,984,964,1,0,0,0,984,969,1,0, + 0,0,984,974,1,0,0,0,984,979,1,0,0,0,985,115,1,0,0,0,986,992,3,124, + 62,0,987,991,3,118,59,0,988,991,3,120,60,0,989,991,3,122,61,0,990, + 987,1,0,0,0,990,988,1,0,0,0,990,989,1,0,0,0,991,994,1,0,0,0,992, + 990,1,0,0,0,992,993,1,0,0,0,993,117,1,0,0,0,994,992,1,0,0,0,995, + 996,5,125,0,0,996,997,5,77,0,0,997,998,5,125,0,0,998,1006,5,61,0, + 0,999,1000,5,125,0,0,1000,1001,5,78,0,0,1001,1002,5,125,0,0,1002, + 1006,5,61,0,0,1003,1004,5,125,0,0,1004,1006,5,79,0,0,1005,995,1, + 0,0,0,1005,999,1,0,0,0,1005,1003,1,0,0,0,1006,1008,1,0,0,0,1007, + 1009,5,125,0,0,1008,1007,1,0,0,0,1008,1009,1,0,0,0,1009,1010,1,0, + 0,0,1010,1011,3,124,62,0,1011,119,1,0,0,0,1012,1013,5,125,0,0,1013, + 1015,5,80,0,0,1014,1016,5,125,0,0,1015,1014,1,0,0,0,1015,1016,1, + 0,0,0,1016,1017,1,0,0,0,1017,1018,3,124,62,0,1018,121,1,0,0,0,1019, + 1020,5,125,0,0,1020,1021,5,81,0,0,1021,1022,5,125,0,0,1022,1030, + 5,82,0,0,1023,1024,5,125,0,0,1024,1025,5,81,0,0,1025,1026,5,125, + 0,0,1026,1027,5,76,0,0,1027,1028,5,125,0,0,1028,1030,5,82,0,0,1029, + 1019,1,0,0,0,1029,1023,1,0,0,0,1030,123,1,0,0,0,1031,1050,3,126, + 63,0,1032,1034,5,125,0,0,1033,1032,1,0,0,0,1033,1034,1,0,0,0,1034, + 1035,1,0,0,0,1035,1037,5,18,0,0,1036,1038,5,125,0,0,1037,1036,1, + 0,0,0,1037,1038,1,0,0,0,1038,1039,1,0,0,0,1039,1049,3,126,63,0,1040, + 1042,5,125,0,0,1041,1040,1,0,0,0,1041,1042,1,0,0,0,1042,1043,1,0, + 0,0,1043,1045,5,19,0,0,1044,1046,5,125,0,0,1045,1044,1,0,0,0,1045, + 1046,1,0,0,0,1046,1047,1,0,0,0,1047,1049,3,126,63,0,1048,1033,1, + 0,0,0,1048,1041,1,0,0,0,1049,1052,1,0,0,0,1050,1048,1,0,0,0,1050, + 1051,1,0,0,0,1051,125,1,0,0,0,1052,1050,1,0,0,0,1053,1080,3,128, + 64,0,1054,1056,5,125,0,0,1055,1054,1,0,0,0,1055,1056,1,0,0,0,1056, + 1057,1,0,0,0,1057,1059,5,5,0,0,1058,1060,5,125,0,0,1059,1058,1,0, + 0,0,1059,1060,1,0,0,0,1060,1061,1,0,0,0,1061,1079,3,128,64,0,1062, + 1064,5,125,0,0,1063,1062,1,0,0,0,1063,1064,1,0,0,0,1064,1065,1,0, + 0,0,1065,1067,5,20,0,0,1066,1068,5,125,0,0,1067,1066,1,0,0,0,1067, + 1068,1,0,0,0,1068,1069,1,0,0,0,1069,1079,3,128,64,0,1070,1072,5, + 125,0,0,1071,1070,1,0,0,0,1071,1072,1,0,0,0,1072,1073,1,0,0,0,1073, + 1075,5,21,0,0,1074,1076,5,125,0,0,1075,1074,1,0,0,0,1075,1076,1, + 0,0,0,1076,1077,1,0,0,0,1077,1079,3,128,64,0,1078,1055,1,0,0,0,1078, + 1063,1,0,0,0,1078,1071,1,0,0,0,1079,1082,1,0,0,0,1080,1078,1,0,0, + 0,1080,1081,1,0,0,0,1081,127,1,0,0,0,1082,1080,1,0,0,0,1083,1094, + 3,130,65,0,1084,1086,5,125,0,0,1085,1084,1,0,0,0,1085,1086,1,0,0, + 0,1086,1087,1,0,0,0,1087,1089,5,22,0,0,1088,1090,5,125,0,0,1089, + 1088,1,0,0,0,1089,1090,1,0,0,0,1090,1091,1,0,0,0,1091,1093,3,130, + 65,0,1092,1085,1,0,0,0,1093,1096,1,0,0,0,1094,1092,1,0,0,0,1094, + 1095,1,0,0,0,1095,129,1,0,0,0,1096,1094,1,0,0,0,1097,1104,3,132, + 66,0,1098,1100,7,1,0,0,1099,1101,5,125,0,0,1100,1099,1,0,0,0,1100, + 1101,1,0,0,0,1101,1102,1,0,0,0,1102,1104,3,132,66,0,1103,1097,1, + 0,0,0,1103,1098,1,0,0,0,1104,131,1,0,0,0,1105,1116,3,138,69,0,1106, + 1108,5,125,0,0,1107,1106,1,0,0,0,1107,1108,1,0,0,0,1108,1109,1,0, + 0,0,1109,1115,3,134,67,0,1110,1112,5,125,0,0,1111,1110,1,0,0,0,1111, + 1112,1,0,0,0,1112,1113,1,0,0,0,1113,1115,3,136,68,0,1114,1107,1, + 0,0,0,1114,1111,1,0,0,0,1115,1118,1,0,0,0,1116,1114,1,0,0,0,1116, + 1117,1,0,0,0,1117,1123,1,0,0,0,1118,1116,1,0,0,0,1119,1121,5,125, + 0,0,1120,1119,1,0,0,0,1120,1121,1,0,0,0,1121,1122,1,0,0,0,1122,1124, + 3,90,45,0,1123,1120,1,0,0,0,1123,1124,1,0,0,0,1124,133,1,0,0,0,1125, + 1126,5,8,0,0,1126,1127,3,102,51,0,1127,1128,5,9,0,0,1128,1139,1, + 0,0,0,1129,1131,5,8,0,0,1130,1132,3,102,51,0,1131,1130,1,0,0,0,1131, + 1132,1,0,0,0,1132,1133,1,0,0,0,1133,1135,5,12,0,0,1134,1136,3,102, + 51,0,1135,1134,1,0,0,0,1135,1136,1,0,0,0,1136,1137,1,0,0,0,1137, + 1139,5,9,0,0,1138,1125,1,0,0,0,1138,1129,1,0,0,0,1139,135,1,0,0, + 0,1140,1142,5,23,0,0,1141,1143,5,125,0,0,1142,1141,1,0,0,0,1142, + 1143,1,0,0,0,1143,1144,1,0,0,0,1144,1145,3,190,95,0,1145,137,1,0, + 0,0,1146,1171,3,176,88,0,1147,1171,3,192,96,0,1148,1171,3,140,70, + 0,1149,1151,5,83,0,0,1150,1152,5,125,0,0,1151,1150,1,0,0,0,1151, + 1152,1,0,0,0,1152,1153,1,0,0,0,1153,1155,5,6,0,0,1154,1156,5,125, + 0,0,1155,1154,1,0,0,0,1155,1156,1,0,0,0,1156,1157,1,0,0,0,1157,1159, + 5,5,0,0,1158,1160,5,125,0,0,1159,1158,1,0,0,0,1159,1160,1,0,0,0, + 1160,1161,1,0,0,0,1161,1171,5,7,0,0,1162,1171,3,144,72,0,1163,1171, + 3,146,73,0,1164,1171,3,148,74,0,1165,1171,3,152,76,0,1166,1171,3, + 154,77,0,1167,1171,3,158,79,0,1168,1171,3,162,81,0,1169,1171,3,174, + 87,0,1170,1146,1,0,0,0,1170,1147,1,0,0,0,1170,1148,1,0,0,0,1170, + 1149,1,0,0,0,1170,1162,1,0,0,0,1170,1163,1,0,0,0,1170,1164,1,0,0, + 0,1170,1165,1,0,0,0,1170,1166,1,0,0,0,1170,1167,1,0,0,0,1170,1168, + 1,0,0,0,1170,1169,1,0,0,0,1171,139,1,0,0,0,1172,1177,5,84,0,0,1173, + 1175,5,125,0,0,1174,1173,1,0,0,0,1174,1175,1,0,0,0,1175,1176,1,0, + 0,0,1176,1178,3,142,71,0,1177,1174,1,0,0,0,1178,1179,1,0,0,0,1179, + 1177,1,0,0,0,1179,1180,1,0,0,0,1180,1195,1,0,0,0,1181,1183,5,84, + 0,0,1182,1184,5,125,0,0,1183,1182,1,0,0,0,1183,1184,1,0,0,0,1184, + 1185,1,0,0,0,1185,1190,3,102,51,0,1186,1188,5,125,0,0,1187,1186, + 1,0,0,0,1187,1188,1,0,0,0,1188,1189,1,0,0,0,1189,1191,3,142,71,0, + 1190,1187,1,0,0,0,1191,1192,1,0,0,0,1192,1190,1,0,0,0,1192,1193, + 1,0,0,0,1193,1195,1,0,0,0,1194,1172,1,0,0,0,1194,1181,1,0,0,0,1195, + 1204,1,0,0,0,1196,1198,5,125,0,0,1197,1196,1,0,0,0,1197,1198,1,0, + 0,0,1198,1199,1,0,0,0,1199,1201,5,85,0,0,1200,1202,5,125,0,0,1201, + 1200,1,0,0,0,1201,1202,1,0,0,0,1202,1203,1,0,0,0,1203,1205,3,102, + 51,0,1204,1197,1,0,0,0,1204,1205,1,0,0,0,1205,1207,1,0,0,0,1206, + 1208,5,125,0,0,1207,1206,1,0,0,0,1207,1208,1,0,0,0,1208,1209,1,0, + 0,0,1209,1210,5,86,0,0,1210,141,1,0,0,0,1211,1213,5,87,0,0,1212, + 1214,5,125,0,0,1213,1212,1,0,0,0,1213,1214,1,0,0,0,1214,1215,1,0, + 0,0,1215,1217,3,102,51,0,1216,1218,5,125,0,0,1217,1216,1,0,0,0,1217, + 1218,1,0,0,0,1218,1219,1,0,0,0,1219,1221,5,88,0,0,1220,1222,5,125, + 0,0,1221,1220,1,0,0,0,1221,1222,1,0,0,0,1222,1223,1,0,0,0,1223,1224, + 3,102,51,0,1224,143,1,0,0,0,1225,1227,5,8,0,0,1226,1228,5,125,0, + 0,1227,1226,1,0,0,0,1227,1228,1,0,0,0,1228,1229,1,0,0,0,1229,1238, + 3,150,75,0,1230,1232,5,125,0,0,1231,1230,1,0,0,0,1231,1232,1,0,0, + 0,1232,1233,1,0,0,0,1233,1235,5,11,0,0,1234,1236,5,125,0,0,1235, + 1234,1,0,0,0,1235,1236,1,0,0,0,1236,1237,1,0,0,0,1237,1239,3,102, + 51,0,1238,1231,1,0,0,0,1238,1239,1,0,0,0,1239,1241,1,0,0,0,1240, + 1242,5,125,0,0,1241,1240,1,0,0,0,1241,1242,1,0,0,0,1242,1243,1,0, + 0,0,1243,1244,5,9,0,0,1244,145,1,0,0,0,1245,1247,5,8,0,0,1246,1248, + 5,125,0,0,1247,1246,1,0,0,0,1247,1248,1,0,0,0,1248,1257,1,0,0,0, + 1249,1251,3,174,87,0,1250,1252,5,125,0,0,1251,1250,1,0,0,0,1251, + 1252,1,0,0,0,1252,1253,1,0,0,0,1253,1255,5,3,0,0,1254,1256,5,125, + 0,0,1255,1254,1,0,0,0,1255,1256,1,0,0,0,1256,1258,1,0,0,0,1257,1249, + 1,0,0,0,1257,1258,1,0,0,0,1258,1259,1,0,0,0,1259,1261,3,76,38,0, + 1260,1262,5,125,0,0,1261,1260,1,0,0,0,1261,1262,1,0,0,0,1262,1267, + 1,0,0,0,1263,1265,3,66,33,0,1264,1266,5,125,0,0,1265,1264,1,0,0, + 0,1265,1266,1,0,0,0,1266,1268,1,0,0,0,1267,1263,1,0,0,0,1267,1268, + 1,0,0,0,1268,1269,1,0,0,0,1269,1271,5,11,0,0,1270,1272,5,125,0,0, + 1271,1270,1,0,0,0,1271,1272,1,0,0,0,1272,1273,1,0,0,0,1273,1275, + 3,102,51,0,1274,1276,5,125,0,0,1275,1274,1,0,0,0,1275,1276,1,0,0, + 0,1276,1277,1,0,0,0,1277,1278,5,9,0,0,1278,147,1,0,0,0,1279,1281, + 5,47,0,0,1280,1282,5,125,0,0,1281,1280,1,0,0,0,1281,1282,1,0,0,0, + 1282,1283,1,0,0,0,1283,1285,5,6,0,0,1284,1286,5,125,0,0,1285,1284, + 1,0,0,0,1285,1286,1,0,0,0,1286,1287,1,0,0,0,1287,1289,3,150,75,0, + 1288,1290,5,125,0,0,1289,1288,1,0,0,0,1289,1290,1,0,0,0,1290,1291, + 1,0,0,0,1291,1292,5,7,0,0,1292,1336,1,0,0,0,1293,1295,5,89,0,0,1294, + 1296,5,125,0,0,1295,1294,1,0,0,0,1295,1296,1,0,0,0,1296,1297,1,0, + 0,0,1297,1299,5,6,0,0,1298,1300,5,125,0,0,1299,1298,1,0,0,0,1299, + 1300,1,0,0,0,1300,1301,1,0,0,0,1301,1303,3,150,75,0,1302,1304,5, + 125,0,0,1303,1302,1,0,0,0,1303,1304,1,0,0,0,1304,1305,1,0,0,0,1305, + 1306,5,7,0,0,1306,1336,1,0,0,0,1307,1309,5,90,0,0,1308,1310,5,125, + 0,0,1309,1308,1,0,0,0,1309,1310,1,0,0,0,1310,1311,1,0,0,0,1311,1313, + 5,6,0,0,1312,1314,5,125,0,0,1313,1312,1,0,0,0,1313,1314,1,0,0,0, + 1314,1315,1,0,0,0,1315,1317,3,150,75,0,1316,1318,5,125,0,0,1317, + 1316,1,0,0,0,1317,1318,1,0,0,0,1318,1319,1,0,0,0,1319,1320,5,7,0, + 0,1320,1336,1,0,0,0,1321,1323,5,91,0,0,1322,1324,5,125,0,0,1323, + 1322,1,0,0,0,1323,1324,1,0,0,0,1324,1325,1,0,0,0,1325,1327,5,6,0, + 0,1326,1328,5,125,0,0,1327,1326,1,0,0,0,1327,1328,1,0,0,0,1328,1329, + 1,0,0,0,1329,1331,3,150,75,0,1330,1332,5,125,0,0,1331,1330,1,0,0, + 0,1331,1332,1,0,0,0,1332,1333,1,0,0,0,1333,1334,5,7,0,0,1334,1336, + 1,0,0,0,1335,1279,1,0,0,0,1335,1293,1,0,0,0,1335,1307,1,0,0,0,1335, + 1321,1,0,0,0,1336,149,1,0,0,0,1337,1342,3,156,78,0,1338,1340,5,125, + 0,0,1339,1338,1,0,0,0,1339,1340,1,0,0,0,1340,1341,1,0,0,0,1341,1343, + 3,66,33,0,1342,1339,1,0,0,0,1342,1343,1,0,0,0,1343,151,1,0,0,0,1344, + 1345,3,76,38,0,1345,153,1,0,0,0,1346,1348,5,6,0,0,1347,1349,5,125, + 0,0,1348,1347,1,0,0,0,1348,1349,1,0,0,0,1349,1350,1,0,0,0,1350,1352, + 3,102,51,0,1351,1353,5,125,0,0,1352,1351,1,0,0,0,1352,1353,1,0,0, + 0,1353,1354,1,0,0,0,1354,1355,5,7,0,0,1355,155,1,0,0,0,1356,1357, + 3,174,87,0,1357,1358,5,125,0,0,1358,1359,5,80,0,0,1359,1360,5,125, + 0,0,1360,1361,3,102,51,0,1361,157,1,0,0,0,1362,1364,3,160,80,0,1363, + 1365,5,125,0,0,1364,1363,1,0,0,0,1364,1365,1,0,0,0,1365,1366,1,0, + 0,0,1366,1368,5,6,0,0,1367,1369,5,125,0,0,1368,1367,1,0,0,0,1368, + 1369,1,0,0,0,1369,1374,1,0,0,0,1370,1372,5,63,0,0,1371,1373,5,125, + 0,0,1372,1371,1,0,0,0,1372,1373,1,0,0,0,1373,1375,1,0,0,0,1374,1370, + 1,0,0,0,1374,1375,1,0,0,0,1375,1393,1,0,0,0,1376,1378,3,102,51,0, + 1377,1379,5,125,0,0,1378,1377,1,0,0,0,1378,1379,1,0,0,0,1379,1390, + 1,0,0,0,1380,1382,5,2,0,0,1381,1383,5,125,0,0,1382,1381,1,0,0,0, + 1382,1383,1,0,0,0,1383,1384,1,0,0,0,1384,1386,3,102,51,0,1385,1387, + 5,125,0,0,1386,1385,1,0,0,0,1386,1387,1,0,0,0,1387,1389,1,0,0,0, + 1388,1380,1,0,0,0,1389,1392,1,0,0,0,1390,1388,1,0,0,0,1390,1391, + 1,0,0,0,1391,1394,1,0,0,0,1392,1390,1,0,0,0,1393,1376,1,0,0,0,1393, + 1394,1,0,0,0,1394,1395,1,0,0,0,1395,1396,5,7,0,0,1396,159,1,0,0, + 0,1397,1398,3,172,86,0,1398,1399,3,198,99,0,1399,161,1,0,0,0,1400, + 1402,5,92,0,0,1401,1403,5,125,0,0,1402,1401,1,0,0,0,1402,1403,1, + 0,0,0,1403,1404,1,0,0,0,1404,1406,5,24,0,0,1405,1407,5,125,0,0,1406, + 1405,1,0,0,0,1406,1407,1,0,0,0,1407,1416,1,0,0,0,1408,1417,3,6,3, + 0,1409,1414,3,68,34,0,1410,1412,5,125,0,0,1411,1410,1,0,0,0,1411, + 1412,1,0,0,0,1412,1413,1,0,0,0,1413,1415,3,66,33,0,1414,1411,1,0, + 0,0,1414,1415,1,0,0,0,1415,1417,1,0,0,0,1416,1408,1,0,0,0,1416,1409, + 1,0,0,0,1417,1419,1,0,0,0,1418,1420,5,125,0,0,1419,1418,1,0,0,0, + 1419,1420,1,0,0,0,1420,1421,1,0,0,0,1421,1422,5,25,0,0,1422,163, + 1,0,0,0,1423,1425,3,170,85,0,1424,1426,5,125,0,0,1425,1424,1,0,0, + 0,1425,1426,1,0,0,0,1426,1427,1,0,0,0,1427,1429,5,6,0,0,1428,1430, + 5,125,0,0,1429,1428,1,0,0,0,1429,1430,1,0,0,0,1430,1448,1,0,0,0, + 1431,1433,3,102,51,0,1432,1434,5,125,0,0,1433,1432,1,0,0,0,1433, + 1434,1,0,0,0,1434,1445,1,0,0,0,1435,1437,5,2,0,0,1436,1438,5,125, + 0,0,1437,1436,1,0,0,0,1437,1438,1,0,0,0,1438,1439,1,0,0,0,1439,1441, + 3,102,51,0,1440,1442,5,125,0,0,1441,1440,1,0,0,0,1441,1442,1,0,0, + 0,1442,1444,1,0,0,0,1443,1435,1,0,0,0,1444,1447,1,0,0,0,1445,1443, + 1,0,0,0,1445,1446,1,0,0,0,1446,1449,1,0,0,0,1447,1445,1,0,0,0,1448, + 1431,1,0,0,0,1448,1449,1,0,0,0,1449,1450,1,0,0,0,1450,1451,5,7,0, + 0,1451,165,1,0,0,0,1452,1453,3,170,85,0,1453,167,1,0,0,0,1454,1455, + 3,198,99,0,1455,169,1,0,0,0,1456,1457,3,172,86,0,1457,1458,3,198, + 99,0,1458,171,1,0,0,0,1459,1460,3,198,99,0,1460,1461,5,23,0,0,1461, + 1463,1,0,0,0,1462,1459,1,0,0,0,1463,1466,1,0,0,0,1464,1462,1,0,0, + 0,1464,1465,1,0,0,0,1465,173,1,0,0,0,1466,1464,1,0,0,0,1467,1468, + 3,198,99,0,1468,175,1,0,0,0,1469,1476,3,178,89,0,1470,1476,5,82, + 0,0,1471,1476,3,180,90,0,1472,1476,5,107,0,0,1473,1476,3,186,93, + 0,1474,1476,3,188,94,0,1475,1469,1,0,0,0,1475,1470,1,0,0,0,1475, + 1471,1,0,0,0,1475,1472,1,0,0,0,1475,1473,1,0,0,0,1475,1474,1,0,0, + 0,1476,177,1,0,0,0,1477,1478,7,2,0,0,1478,179,1,0,0,0,1479,1482, + 3,184,92,0,1480,1482,3,182,91,0,1481,1479,1,0,0,0,1481,1480,1,0, + 0,0,1482,181,1,0,0,0,1483,1484,7,3,0,0,1484,183,1,0,0,0,1485,1486, + 7,4,0,0,1486,185,1,0,0,0,1487,1489,5,8,0,0,1488,1490,5,125,0,0,1489, + 1488,1,0,0,0,1489,1490,1,0,0,0,1490,1508,1,0,0,0,1491,1493,3,102, + 51,0,1492,1494,5,125,0,0,1493,1492,1,0,0,0,1493,1494,1,0,0,0,1494, + 1505,1,0,0,0,1495,1497,5,2,0,0,1496,1498,5,125,0,0,1497,1496,1,0, + 0,0,1497,1498,1,0,0,0,1498,1499,1,0,0,0,1499,1501,3,102,51,0,1500, + 1502,5,125,0,0,1501,1500,1,0,0,0,1501,1502,1,0,0,0,1502,1504,1,0, + 0,0,1503,1495,1,0,0,0,1504,1507,1,0,0,0,1505,1503,1,0,0,0,1505,1506, + 1,0,0,0,1506,1509,1,0,0,0,1507,1505,1,0,0,0,1508,1491,1,0,0,0,1508, + 1509,1,0,0,0,1509,1510,1,0,0,0,1510,1511,5,9,0,0,1511,187,1,0,0, + 0,1512,1514,5,24,0,0,1513,1515,5,125,0,0,1514,1513,1,0,0,0,1514, + 1515,1,0,0,0,1515,1549,1,0,0,0,1516,1518,3,190,95,0,1517,1519,5, + 125,0,0,1518,1517,1,0,0,0,1518,1519,1,0,0,0,1519,1520,1,0,0,0,1520, + 1522,5,10,0,0,1521,1523,5,125,0,0,1522,1521,1,0,0,0,1522,1523,1, + 0,0,0,1523,1524,1,0,0,0,1524,1526,3,102,51,0,1525,1527,5,125,0,0, + 1526,1525,1,0,0,0,1526,1527,1,0,0,0,1527,1546,1,0,0,0,1528,1530, + 5,2,0,0,1529,1531,5,125,0,0,1530,1529,1,0,0,0,1530,1531,1,0,0,0, + 1531,1532,1,0,0,0,1532,1534,3,190,95,0,1533,1535,5,125,0,0,1534, + 1533,1,0,0,0,1534,1535,1,0,0,0,1535,1536,1,0,0,0,1536,1538,5,10, + 0,0,1537,1539,5,125,0,0,1538,1537,1,0,0,0,1538,1539,1,0,0,0,1539, + 1540,1,0,0,0,1540,1542,3,102,51,0,1541,1543,5,125,0,0,1542,1541, + 1,0,0,0,1542,1543,1,0,0,0,1543,1545,1,0,0,0,1544,1528,1,0,0,0,1545, + 1548,1,0,0,0,1546,1544,1,0,0,0,1546,1547,1,0,0,0,1547,1550,1,0,0, + 0,1548,1546,1,0,0,0,1549,1516,1,0,0,0,1549,1550,1,0,0,0,1550,1551, + 1,0,0,0,1551,1552,5,25,0,0,1552,189,1,0,0,0,1553,1554,3,194,97,0, + 1554,191,1,0,0,0,1555,1558,5,26,0,0,1556,1559,3,198,99,0,1557,1559, + 5,96,0,0,1558,1556,1,0,0,0,1558,1557,1,0,0,0,1559,193,1,0,0,0,1560, + 1563,3,198,99,0,1561,1563,3,196,98,0,1562,1560,1,0,0,0,1562,1561, + 1,0,0,0,1563,195,1,0,0,0,1564,1565,7,5,0,0,1565,197,1,0,0,0,1566, + 1567,7,6,0,0,1567,199,1,0,0,0,1568,1569,7,7,0,0,1569,201,1,0,0,0, + 1570,1571,7,8,0,0,1571,203,1,0,0,0,1572,1573,7,9,0,0,1573,205,1, + 0,0,0,291,207,211,214,217,225,229,234,241,246,249,253,257,261,267, + 271,276,281,285,288,290,294,298,303,307,312,316,325,330,334,338, + 342,345,349,359,366,379,383,389,393,397,402,407,411,417,421,427, + 431,437,441,445,449,453,457,462,469,473,478,485,491,496,502,505, + 511,513,517,521,526,530,533,540,547,550,556,559,565,569,573,577, + 581,586,591,595,600,603,612,621,626,639,642,650,654,659,664,668, + 673,679,684,691,695,700,704,708,710,714,716,720,722,728,734,738, + 741,744,748,754,758,761,764,770,773,776,780,786,789,792,796,800, + 804,806,810,812,815,819,821,827,831,835,839,842,847,852,857,862, + 868,872,874,878,882,884,886,894,899,910,920,930,935,939,946,951, + 956,961,966,971,976,981,984,990,992,1005,1008,1015,1029,1033,1037, + 1041,1045,1048,1050,1055,1059,1063,1067,1071,1075,1078,1080,1085, + 1089,1094,1100,1103,1107,1111,1114,1116,1120,1123,1131,1135,1138, + 1142,1151,1155,1159,1170,1174,1179,1183,1187,1192,1194,1197,1201, + 1204,1207,1213,1217,1221,1227,1231,1235,1238,1241,1247,1251,1255, + 1257,1261,1265,1267,1271,1275,1281,1285,1289,1295,1299,1303,1309, + 1313,1317,1323,1327,1331,1335,1339,1342,1348,1352,1364,1368,1372, + 1374,1378,1382,1386,1390,1393,1402,1406,1411,1414,1416,1419,1425, + 1429,1433,1437,1441,1445,1448,1464,1475,1481,1489,1493,1497,1501, + 1505,1508,1514,1518,1522,1526,1530,1534,1538,1542,1546,1549,1558, + 1562 + ] + +class CypherParser ( Parser ): + + grammarFileName = "Cypher.g4" + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + sharedContextCache = PredictionContextCache() + + literalNames = [ "", "';'", "','", "'='", "'+='", "'*'", "'('", + "')'", "'['", "']'", "':'", "'|'", "'..'", "'<>'", + "'<'", "'>'", "'<='", "'>='", "'+'", "'-'", "'/'", + "'%'", "'^'", "'.'", "'{'", "'}'", "'$'", "'\\u27E8'", + "'\\u3008'", "'\\uFE64'", "'\\uFF1C'", "'\\u27E9'", + "'\\u3009'", "'\\uFE65'", "'\\uFF1E'", "'\\u00AD'", + "'\\u2010'", "'\\u2011'", "'\\u2012'", "'\\u2013'", + "'\\u2014'", "'\\u2015'", "'\\u2212'", "'\\uFE58'", + "'\\uFE63'", "'\\uFF0D'", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "'0'" ] + + symbolicNames = [ "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "UNION", "ALL", "OPTIONAL", + "MATCH", "UNWIND", "AS", "MERGE", "ON", "CREATE", + "SET", "DETACH", "DELETE", "REMOVE", "CALL", "YIELD", + "WITH", "RETURN", "DISTINCT", "ORDER", "BY", "L_SKIP", + "LIMIT", "ASCENDING", "ASC", "DESCENDING", "DESC", + "WHERE", "OR", "XOR", "AND", "NOT", "STARTS", "ENDS", + "CONTAINS", "IN", "IS", "NULL", "COUNT", "CASE", "ELSE", + "END", "WHEN", "THEN", "ANY", "NONE", "SINGLE", "EXISTS", + "TRUE", "FALSE", "HexInteger", "DecimalInteger", "OctalInteger", + "HexLetter", "HexDigit", "Digit", "NonZeroDigit", + "NonZeroOctDigit", "OctDigit", "ZeroDigit", "ExponentDecimalReal", + "RegularDecimalReal", "StringLiteral", "EscapedChar", + "CONSTRAINT", "DO", "FOR", "REQUIRE", "UNIQUE", "MANDATORY", + "SCALAR", "OF", "ADD", "DROP", "FILTER", "EXTRACT", + "UnescapedSymbolicName", "IdentifierStart", "IdentifierPart", + "EscapedSymbolicName", "SP", "WHITESPACE", "Comment" ] + + RULE_oC_Cypher = 0 + RULE_oC_Statement = 1 + RULE_oC_Query = 2 + RULE_oC_RegularQuery = 3 + RULE_oC_Union = 4 + RULE_oC_SingleQuery = 5 + RULE_oC_SinglePartQuery = 6 + RULE_oC_MultiPartQuery = 7 + RULE_oC_UpdatingClause = 8 + RULE_oC_ReadingClause = 9 + RULE_oC_Match = 10 + RULE_oC_Unwind = 11 + RULE_oC_Merge = 12 + RULE_oC_MergeAction = 13 + RULE_oC_Create = 14 + RULE_oC_Set = 15 + RULE_oC_SetItem = 16 + RULE_oC_Delete = 17 + RULE_oC_Remove = 18 + RULE_oC_RemoveItem = 19 + RULE_oC_InQueryCall = 20 + RULE_oC_StandaloneCall = 21 + RULE_oC_YieldItems = 22 + RULE_oC_YieldItem = 23 + RULE_oC_With = 24 + RULE_oC_Return = 25 + RULE_oC_ProjectionBody = 26 + RULE_oC_ProjectionItems = 27 + RULE_oC_ProjectionItem = 28 + RULE_oC_Order = 29 + RULE_oC_Skip = 30 + RULE_oC_Limit = 31 + RULE_oC_SortItem = 32 + RULE_oC_Where = 33 + RULE_oC_Pattern = 34 + RULE_oC_PatternPart = 35 + RULE_oC_AnonymousPatternPart = 36 + RULE_oC_PatternElement = 37 + RULE_oC_RelationshipsPattern = 38 + RULE_oC_NodePattern = 39 + RULE_oC_PatternElementChain = 40 + RULE_oC_RelationshipPattern = 41 + RULE_oC_RelationshipDetail = 42 + RULE_oC_Properties = 43 + RULE_oC_RelationshipTypes = 44 + RULE_oC_NodeLabels = 45 + RULE_oC_NodeLabel = 46 + RULE_oC_RangeLiteral = 47 + RULE_oC_LabelName = 48 + RULE_oC_RelTypeName = 49 + RULE_oC_PropertyExpression = 50 + RULE_oC_Expression = 51 + RULE_oC_OrExpression = 52 + RULE_oC_XorExpression = 53 + RULE_oC_AndExpression = 54 + RULE_oC_NotExpression = 55 + RULE_oC_ComparisonExpression = 56 + RULE_oC_PartialComparisonExpression = 57 + RULE_oC_StringListNullPredicateExpression = 58 + RULE_oC_StringPredicateExpression = 59 + RULE_oC_ListPredicateExpression = 60 + RULE_oC_NullPredicateExpression = 61 + RULE_oC_AddOrSubtractExpression = 62 + RULE_oC_MultiplyDivideModuloExpression = 63 + RULE_oC_PowerOfExpression = 64 + RULE_oC_UnaryAddOrSubtractExpression = 65 + RULE_oC_NonArithmeticOperatorExpression = 66 + RULE_oC_ListOperatorExpression = 67 + RULE_oC_PropertyLookup = 68 + RULE_oC_Atom = 69 + RULE_oC_CaseExpression = 70 + RULE_oC_CaseAlternative = 71 + RULE_oC_ListComprehension = 72 + RULE_oC_PatternComprehension = 73 + RULE_oC_Quantifier = 74 + RULE_oC_FilterExpression = 75 + RULE_oC_PatternPredicate = 76 + RULE_oC_ParenthesizedExpression = 77 + RULE_oC_IdInColl = 78 + RULE_oC_FunctionInvocation = 79 + RULE_oC_FunctionName = 80 + RULE_oC_ExistentialSubquery = 81 + RULE_oC_ExplicitProcedureInvocation = 82 + RULE_oC_ImplicitProcedureInvocation = 83 + RULE_oC_ProcedureResultField = 84 + RULE_oC_ProcedureName = 85 + RULE_oC_Namespace = 86 + RULE_oC_Variable = 87 + RULE_oC_Literal = 88 + RULE_oC_BooleanLiteral = 89 + RULE_oC_NumberLiteral = 90 + RULE_oC_IntegerLiteral = 91 + RULE_oC_DoubleLiteral = 92 + RULE_oC_ListLiteral = 93 + RULE_oC_MapLiteral = 94 + RULE_oC_PropertyKeyName = 95 + RULE_oC_Parameter = 96 + RULE_oC_SchemaName = 97 + RULE_oC_ReservedWord = 98 + RULE_oC_SymbolicName = 99 + RULE_oC_LeftArrowHead = 100 + RULE_oC_RightArrowHead = 101 + RULE_oC_Dash = 102 + + ruleNames = [ "oC_Cypher", "oC_Statement", "oC_Query", "oC_RegularQuery", + "oC_Union", "oC_SingleQuery", "oC_SinglePartQuery", "oC_MultiPartQuery", + "oC_UpdatingClause", "oC_ReadingClause", "oC_Match", + "oC_Unwind", "oC_Merge", "oC_MergeAction", "oC_Create", + "oC_Set", "oC_SetItem", "oC_Delete", "oC_Remove", "oC_RemoveItem", + "oC_InQueryCall", "oC_StandaloneCall", "oC_YieldItems", + "oC_YieldItem", "oC_With", "oC_Return", "oC_ProjectionBody", + "oC_ProjectionItems", "oC_ProjectionItem", "oC_Order", + "oC_Skip", "oC_Limit", "oC_SortItem", "oC_Where", "oC_Pattern", + "oC_PatternPart", "oC_AnonymousPatternPart", "oC_PatternElement", + "oC_RelationshipsPattern", "oC_NodePattern", "oC_PatternElementChain", + "oC_RelationshipPattern", "oC_RelationshipDetail", "oC_Properties", + "oC_RelationshipTypes", "oC_NodeLabels", "oC_NodeLabel", + "oC_RangeLiteral", "oC_LabelName", "oC_RelTypeName", + "oC_PropertyExpression", "oC_Expression", "oC_OrExpression", + "oC_XorExpression", "oC_AndExpression", "oC_NotExpression", + "oC_ComparisonExpression", "oC_PartialComparisonExpression", + "oC_StringListNullPredicateExpression", "oC_StringPredicateExpression", + "oC_ListPredicateExpression", "oC_NullPredicateExpression", + "oC_AddOrSubtractExpression", "oC_MultiplyDivideModuloExpression", + "oC_PowerOfExpression", "oC_UnaryAddOrSubtractExpression", + "oC_NonArithmeticOperatorExpression", "oC_ListOperatorExpression", + "oC_PropertyLookup", "oC_Atom", "oC_CaseExpression", + "oC_CaseAlternative", "oC_ListComprehension", "oC_PatternComprehension", + "oC_Quantifier", "oC_FilterExpression", "oC_PatternPredicate", + "oC_ParenthesizedExpression", "oC_IdInColl", "oC_FunctionInvocation", + "oC_FunctionName", "oC_ExistentialSubquery", "oC_ExplicitProcedureInvocation", + "oC_ImplicitProcedureInvocation", "oC_ProcedureResultField", + "oC_ProcedureName", "oC_Namespace", "oC_Variable", "oC_Literal", + "oC_BooleanLiteral", "oC_NumberLiteral", "oC_IntegerLiteral", + "oC_DoubleLiteral", "oC_ListLiteral", "oC_MapLiteral", + "oC_PropertyKeyName", "oC_Parameter", "oC_SchemaName", + "oC_ReservedWord", "oC_SymbolicName", "oC_LeftArrowHead", + "oC_RightArrowHead", "oC_Dash" ] + + EOF = Token.EOF + T__0=1 + T__1=2 + T__2=3 + T__3=4 + T__4=5 + T__5=6 + T__6=7 + T__7=8 + T__8=9 + T__9=10 + T__10=11 + T__11=12 + T__12=13 + T__13=14 + T__14=15 + T__15=16 + T__16=17 + T__17=18 + T__18=19 + T__19=20 + T__20=21 + T__21=22 + T__22=23 + T__23=24 + T__24=25 + T__25=26 + T__26=27 + T__27=28 + T__28=29 + T__29=30 + T__30=31 + T__31=32 + T__32=33 + T__33=34 + T__34=35 + T__35=36 + T__36=37 + T__37=38 + T__38=39 + T__39=40 + T__40=41 + T__41=42 + T__42=43 + T__43=44 + T__44=45 + UNION=46 + ALL=47 + OPTIONAL=48 + MATCH=49 + UNWIND=50 + AS=51 + MERGE=52 + ON=53 + CREATE=54 + SET=55 + DETACH=56 + DELETE=57 + REMOVE=58 + CALL=59 + YIELD=60 + WITH=61 + RETURN=62 + DISTINCT=63 + ORDER=64 + BY=65 + L_SKIP=66 + LIMIT=67 + ASCENDING=68 + ASC=69 + DESCENDING=70 + DESC=71 + WHERE=72 + OR=73 + XOR=74 + AND=75 + NOT=76 + STARTS=77 + ENDS=78 + CONTAINS=79 + IN=80 + IS=81 + NULL=82 + COUNT=83 + CASE=84 + ELSE=85 + END=86 + WHEN=87 + THEN=88 + ANY=89 + NONE=90 + SINGLE=91 + EXISTS=92 + TRUE=93 + FALSE=94 + HexInteger=95 + DecimalInteger=96 + OctalInteger=97 + HexLetter=98 + HexDigit=99 + Digit=100 + NonZeroDigit=101 + NonZeroOctDigit=102 + OctDigit=103 + ZeroDigit=104 + ExponentDecimalReal=105 + RegularDecimalReal=106 + StringLiteral=107 + EscapedChar=108 + CONSTRAINT=109 + DO=110 + FOR=111 + REQUIRE=112 + UNIQUE=113 + MANDATORY=114 + SCALAR=115 + OF=116 + ADD=117 + DROP=118 + FILTER=119 + EXTRACT=120 + UnescapedSymbolicName=121 + IdentifierStart=122 + IdentifierPart=123 + EscapedSymbolicName=124 + SP=125 + WHITESPACE=126 + Comment=127 + + def __init__(self, input:TokenStream, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.13.2") + self._interp = ParserATNSimulator(self, self.atn, self.decisionsToDFA, self.sharedContextCache) + self._predicates = None + + + + + class OC_CypherContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Statement(self): + return self.getTypedRuleContext(CypherParser.OC_StatementContext,0) + + + def EOF(self): + return self.getToken(CypherParser.EOF, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_Cypher + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Cypher" ): + listener.enterOC_Cypher(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Cypher" ): + listener.exitOC_Cypher(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Cypher" ): + return visitor.visitOC_Cypher(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Cypher(self): + + localctx = CypherParser.OC_CypherContext(self, self._ctx, self.state) + self.enterRule(localctx, 0, self.RULE_oC_Cypher) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 207 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 206 + self.match(CypherParser.SP) + + + self.state = 209 + self.oC_Statement() + self.state = 214 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,2,self._ctx) + if la_ == 1: + self.state = 211 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 210 + self.match(CypherParser.SP) + + + self.state = 213 + self.match(CypherParser.T__0) + + + self.state = 217 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 216 + self.match(CypherParser.SP) + + + self.state = 219 + self.match(CypherParser.EOF) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_StatementContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Query(self): + return self.getTypedRuleContext(CypherParser.OC_QueryContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Statement + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Statement" ): + listener.enterOC_Statement(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Statement" ): + listener.exitOC_Statement(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Statement" ): + return visitor.visitOC_Statement(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Statement(self): + + localctx = CypherParser.OC_StatementContext(self, self._ctx, self.state) + self.enterRule(localctx, 2, self.RULE_oC_Statement) + try: + self.enterOuterAlt(localctx, 1) + self.state = 221 + self.oC_Query() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_QueryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_RegularQuery(self): + return self.getTypedRuleContext(CypherParser.OC_RegularQueryContext,0) + + + def oC_StandaloneCall(self): + return self.getTypedRuleContext(CypherParser.OC_StandaloneCallContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Query + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Query" ): + listener.enterOC_Query(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Query" ): + listener.exitOC_Query(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Query" ): + return visitor.visitOC_Query(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Query(self): + + localctx = CypherParser.OC_QueryContext(self, self._ctx, self.state) + self.enterRule(localctx, 4, self.RULE_oC_Query) + try: + self.state = 225 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,4,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 223 + self.oC_RegularQuery() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 224 + self.oC_StandaloneCall() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_RegularQueryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SingleQuery(self): + return self.getTypedRuleContext(CypherParser.OC_SingleQueryContext,0) + + + def oC_Union(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_UnionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_UnionContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_RegularQuery + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_RegularQuery" ): + listener.enterOC_RegularQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_RegularQuery" ): + listener.exitOC_RegularQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_RegularQuery" ): + return visitor.visitOC_RegularQuery(self) + else: + return visitor.visitChildren(self) + + + + + def oC_RegularQuery(self): + + localctx = CypherParser.OC_RegularQueryContext(self, self._ctx, self.state) + self.enterRule(localctx, 6, self.RULE_oC_RegularQuery) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 227 + self.oC_SingleQuery() + self.state = 234 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,6,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 229 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 228 + self.match(CypherParser.SP) + + + self.state = 231 + self.oC_Union() + self.state = 236 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,6,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_UnionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def UNION(self): + return self.getToken(CypherParser.UNION, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def ALL(self): + return self.getToken(CypherParser.ALL, 0) + + def oC_SingleQuery(self): + return self.getTypedRuleContext(CypherParser.OC_SingleQueryContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Union + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Union" ): + listener.enterOC_Union(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Union" ): + listener.exitOC_Union(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Union" ): + return visitor.visitOC_Union(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Union(self): + + localctx = CypherParser.OC_UnionContext(self, self._ctx, self.state) + self.enterRule(localctx, 8, self.RULE_oC_Union) + self._la = 0 # Token type + try: + self.state = 249 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,9,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 237 + self.match(CypherParser.UNION) + self.state = 238 + self.match(CypherParser.SP) + self.state = 239 + self.match(CypherParser.ALL) + self.state = 241 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 240 + self.match(CypherParser.SP) + + + self.state = 243 + self.oC_SingleQuery() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 244 + self.match(CypherParser.UNION) + self.state = 246 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 245 + self.match(CypherParser.SP) + + + self.state = 248 + self.oC_SingleQuery() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_SingleQueryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SinglePartQuery(self): + return self.getTypedRuleContext(CypherParser.OC_SinglePartQueryContext,0) + + + def oC_MultiPartQuery(self): + return self.getTypedRuleContext(CypherParser.OC_MultiPartQueryContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_SingleQuery + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_SingleQuery" ): + listener.enterOC_SingleQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_SingleQuery" ): + listener.exitOC_SingleQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_SingleQuery" ): + return visitor.visitOC_SingleQuery(self) + else: + return visitor.visitChildren(self) + + + + + def oC_SingleQuery(self): + + localctx = CypherParser.OC_SingleQueryContext(self, self._ctx, self.state) + self.enterRule(localctx, 10, self.RULE_oC_SingleQuery) + try: + self.state = 253 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,10,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 251 + self.oC_SinglePartQuery() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 252 + self.oC_MultiPartQuery() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_SinglePartQueryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Return(self): + return self.getTypedRuleContext(CypherParser.OC_ReturnContext,0) + + + def oC_ReadingClause(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ReadingClauseContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ReadingClauseContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_UpdatingClause(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_UpdatingClauseContext) + else: + return self.getTypedRuleContext(CypherParser.OC_UpdatingClauseContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_SinglePartQuery + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_SinglePartQuery" ): + listener.enterOC_SinglePartQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_SinglePartQuery" ): + listener.exitOC_SinglePartQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_SinglePartQuery" ): + return visitor.visitOC_SinglePartQuery(self) + else: + return visitor.visitChildren(self) + + + + + def oC_SinglePartQuery(self): + + localctx = CypherParser.OC_SinglePartQueryContext(self, self._ctx, self.state) + self.enterRule(localctx, 12, self.RULE_oC_SinglePartQuery) + self._la = 0 # Token type + try: + self.state = 290 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,19,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 261 + self._errHandler.sync(self) + _la = self._input.LA(1) + while (((_la) & ~0x3f) == 0 and ((1 << _la) & 578431077140398080) != 0): + self.state = 255 + self.oC_ReadingClause() + self.state = 257 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 256 + self.match(CypherParser.SP) + + + self.state = 263 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 264 + self.oC_Return() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 271 + self._errHandler.sync(self) + _la = self._input.LA(1) + while (((_la) & ~0x3f) == 0 and ((1 << _la) & 578431077140398080) != 0): + self.state = 265 + self.oC_ReadingClause() + self.state = 267 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 266 + self.match(CypherParser.SP) + + + self.state = 273 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 274 + self.oC_UpdatingClause() + self.state = 281 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,16,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 276 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 275 + self.match(CypherParser.SP) + + + self.state = 278 + self.oC_UpdatingClause() + self.state = 283 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,16,self._ctx) + + self.state = 288 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,18,self._ctx) + if la_ == 1: + self.state = 285 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 284 + self.match(CypherParser.SP) + + + self.state = 287 + self.oC_Return() + + + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_MultiPartQueryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SinglePartQuery(self): + return self.getTypedRuleContext(CypherParser.OC_SinglePartQueryContext,0) + + + def oC_With(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_WithContext) + else: + return self.getTypedRuleContext(CypherParser.OC_WithContext,i) + + + def oC_ReadingClause(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ReadingClauseContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ReadingClauseContext,i) + + + def oC_UpdatingClause(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_UpdatingClauseContext) + else: + return self.getTypedRuleContext(CypherParser.OC_UpdatingClauseContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_MultiPartQuery + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_MultiPartQuery" ): + listener.enterOC_MultiPartQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_MultiPartQuery" ): + listener.exitOC_MultiPartQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_MultiPartQuery" ): + return visitor.visitOC_MultiPartQuery(self) + else: + return visitor.visitChildren(self) + + + + + def oC_MultiPartQuery(self): + + localctx = CypherParser.OC_MultiPartQueryContext(self, self._ctx, self.state) + self.enterRule(localctx, 14, self.RULE_oC_MultiPartQuery) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 314 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 298 + self._errHandler.sync(self) + _la = self._input.LA(1) + while (((_la) & ~0x3f) == 0 and ((1 << _la) & 578431077140398080) != 0): + self.state = 292 + self.oC_ReadingClause() + self.state = 294 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 293 + self.match(CypherParser.SP) + + + self.state = 300 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 307 + self._errHandler.sync(self) + _la = self._input.LA(1) + while (((_la) & ~0x3f) == 0 and ((1 << _la) & 562949953421312000) != 0): + self.state = 301 + self.oC_UpdatingClause() + self.state = 303 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 302 + self.match(CypherParser.SP) + + + self.state = 309 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 310 + self.oC_With() + self.state = 312 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 311 + self.match(CypherParser.SP) + + + + else: + raise NoViableAltException(self) + self.state = 316 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,25,self._ctx) + + self.state = 318 + self.oC_SinglePartQuery() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_UpdatingClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Create(self): + return self.getTypedRuleContext(CypherParser.OC_CreateContext,0) + + + def oC_Merge(self): + return self.getTypedRuleContext(CypherParser.OC_MergeContext,0) + + + def oC_Delete(self): + return self.getTypedRuleContext(CypherParser.OC_DeleteContext,0) + + + def oC_Set(self): + return self.getTypedRuleContext(CypherParser.OC_SetContext,0) + + + def oC_Remove(self): + return self.getTypedRuleContext(CypherParser.OC_RemoveContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_UpdatingClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_UpdatingClause" ): + listener.enterOC_UpdatingClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_UpdatingClause" ): + listener.exitOC_UpdatingClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_UpdatingClause" ): + return visitor.visitOC_UpdatingClause(self) + else: + return visitor.visitChildren(self) + + + + + def oC_UpdatingClause(self): + + localctx = CypherParser.OC_UpdatingClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 16, self.RULE_oC_UpdatingClause) + try: + self.state = 325 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [54]: + self.enterOuterAlt(localctx, 1) + self.state = 320 + self.oC_Create() + pass + elif token in [52]: + self.enterOuterAlt(localctx, 2) + self.state = 321 + self.oC_Merge() + pass + elif token in [56, 57]: + self.enterOuterAlt(localctx, 3) + self.state = 322 + self.oC_Delete() + pass + elif token in [55]: + self.enterOuterAlt(localctx, 4) + self.state = 323 + self.oC_Set() + pass + elif token in [58]: + self.enterOuterAlt(localctx, 5) + self.state = 324 + self.oC_Remove() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ReadingClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Match(self): + return self.getTypedRuleContext(CypherParser.OC_MatchContext,0) + + + def oC_Unwind(self): + return self.getTypedRuleContext(CypherParser.OC_UnwindContext,0) + + + def oC_InQueryCall(self): + return self.getTypedRuleContext(CypherParser.OC_InQueryCallContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ReadingClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ReadingClause" ): + listener.enterOC_ReadingClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ReadingClause" ): + listener.exitOC_ReadingClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ReadingClause" ): + return visitor.visitOC_ReadingClause(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ReadingClause(self): + + localctx = CypherParser.OC_ReadingClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 18, self.RULE_oC_ReadingClause) + try: + self.state = 330 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [48, 49]: + self.enterOuterAlt(localctx, 1) + self.state = 327 + self.oC_Match() + pass + elif token in [50]: + self.enterOuterAlt(localctx, 2) + self.state = 328 + self.oC_Unwind() + pass + elif token in [59]: + self.enterOuterAlt(localctx, 3) + self.state = 329 + self.oC_InQueryCall() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_MatchContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def MATCH(self): + return self.getToken(CypherParser.MATCH, 0) + + def oC_Pattern(self): + return self.getTypedRuleContext(CypherParser.OC_PatternContext,0) + + + def OPTIONAL(self): + return self.getToken(CypherParser.OPTIONAL, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_Where(self): + return self.getTypedRuleContext(CypherParser.OC_WhereContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Match + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Match" ): + listener.enterOC_Match(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Match" ): + listener.exitOC_Match(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Match" ): + return visitor.visitOC_Match(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Match(self): + + localctx = CypherParser.OC_MatchContext(self, self._ctx, self.state) + self.enterRule(localctx, 20, self.RULE_oC_Match) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 334 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==48: + self.state = 332 + self.match(CypherParser.OPTIONAL) + self.state = 333 + self.match(CypherParser.SP) + + + self.state = 336 + self.match(CypherParser.MATCH) + self.state = 338 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 337 + self.match(CypherParser.SP) + + + self.state = 340 + self.oC_Pattern() + self.state = 345 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,31,self._ctx) + if la_ == 1: + self.state = 342 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 341 + self.match(CypherParser.SP) + + + self.state = 344 + self.oC_Where() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_UnwindContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def UNWIND(self): + return self.getToken(CypherParser.UNWIND, 0) + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def AS(self): + return self.getToken(CypherParser.AS, 0) + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Unwind + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Unwind" ): + listener.enterOC_Unwind(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Unwind" ): + listener.exitOC_Unwind(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Unwind" ): + return visitor.visitOC_Unwind(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Unwind(self): + + localctx = CypherParser.OC_UnwindContext(self, self._ctx, self.state) + self.enterRule(localctx, 22, self.RULE_oC_Unwind) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 347 + self.match(CypherParser.UNWIND) + self.state = 349 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 348 + self.match(CypherParser.SP) + + + self.state = 351 + self.oC_Expression() + self.state = 352 + self.match(CypherParser.SP) + self.state = 353 + self.match(CypherParser.AS) + self.state = 354 + self.match(CypherParser.SP) + self.state = 355 + self.oC_Variable() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_MergeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def MERGE(self): + return self.getToken(CypherParser.MERGE, 0) + + def oC_PatternPart(self): + return self.getTypedRuleContext(CypherParser.OC_PatternPartContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_MergeAction(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_MergeActionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_MergeActionContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Merge + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Merge" ): + listener.enterOC_Merge(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Merge" ): + listener.exitOC_Merge(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Merge" ): + return visitor.visitOC_Merge(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Merge(self): + + localctx = CypherParser.OC_MergeContext(self, self._ctx, self.state) + self.enterRule(localctx, 24, self.RULE_oC_Merge) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 357 + self.match(CypherParser.MERGE) + self.state = 359 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 358 + self.match(CypherParser.SP) + + + self.state = 361 + self.oC_PatternPart() + self.state = 366 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,34,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 362 + self.match(CypherParser.SP) + self.state = 363 + self.oC_MergeAction() + self.state = 368 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,34,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_MergeActionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ON(self): + return self.getToken(CypherParser.ON, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def MATCH(self): + return self.getToken(CypherParser.MATCH, 0) + + def oC_Set(self): + return self.getTypedRuleContext(CypherParser.OC_SetContext,0) + + + def CREATE(self): + return self.getToken(CypherParser.CREATE, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_MergeAction + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_MergeAction" ): + listener.enterOC_MergeAction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_MergeAction" ): + listener.exitOC_MergeAction(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_MergeAction" ): + return visitor.visitOC_MergeAction(self) + else: + return visitor.visitChildren(self) + + + + + def oC_MergeAction(self): + + localctx = CypherParser.OC_MergeActionContext(self, self._ctx, self.state) + self.enterRule(localctx, 26, self.RULE_oC_MergeAction) + try: + self.state = 379 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,35,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 369 + self.match(CypherParser.ON) + self.state = 370 + self.match(CypherParser.SP) + self.state = 371 + self.match(CypherParser.MATCH) + self.state = 372 + self.match(CypherParser.SP) + self.state = 373 + self.oC_Set() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 374 + self.match(CypherParser.ON) + self.state = 375 + self.match(CypherParser.SP) + self.state = 376 + self.match(CypherParser.CREATE) + self.state = 377 + self.match(CypherParser.SP) + self.state = 378 + self.oC_Set() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_CreateContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def CREATE(self): + return self.getToken(CypherParser.CREATE, 0) + + def oC_Pattern(self): + return self.getTypedRuleContext(CypherParser.OC_PatternContext,0) + + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_Create + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Create" ): + listener.enterOC_Create(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Create" ): + listener.exitOC_Create(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Create" ): + return visitor.visitOC_Create(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Create(self): + + localctx = CypherParser.OC_CreateContext(self, self._ctx, self.state) + self.enterRule(localctx, 28, self.RULE_oC_Create) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 381 + self.match(CypherParser.CREATE) + self.state = 383 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 382 + self.match(CypherParser.SP) + + + self.state = 385 + self.oC_Pattern() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_SetContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SET(self): + return self.getToken(CypherParser.SET, 0) + + def oC_SetItem(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_SetItemContext) + else: + return self.getTypedRuleContext(CypherParser.OC_SetItemContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_Set + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Set" ): + listener.enterOC_Set(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Set" ): + listener.exitOC_Set(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Set" ): + return visitor.visitOC_Set(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Set(self): + + localctx = CypherParser.OC_SetContext(self, self._ctx, self.state) + self.enterRule(localctx, 30, self.RULE_oC_Set) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 387 + self.match(CypherParser.SET) + self.state = 389 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 388 + self.match(CypherParser.SP) + + + self.state = 391 + self.oC_SetItem() + self.state = 402 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,40,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 393 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 392 + self.match(CypherParser.SP) + + + self.state = 395 + self.match(CypherParser.T__1) + self.state = 397 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 396 + self.match(CypherParser.SP) + + + self.state = 399 + self.oC_SetItem() + self.state = 404 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,40,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_SetItemContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_PropertyExpression(self): + return self.getTypedRuleContext(CypherParser.OC_PropertyExpressionContext,0) + + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def oC_NodeLabels(self): + return self.getTypedRuleContext(CypherParser.OC_NodeLabelsContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_SetItem + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_SetItem" ): + listener.enterOC_SetItem(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_SetItem" ): + listener.exitOC_SetItem(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_SetItem" ): + return visitor.visitOC_SetItem(self) + else: + return visitor.visitChildren(self) + + + + + def oC_SetItem(self): + + localctx = CypherParser.OC_SetItemContext(self, self._ctx, self.state) + self.enterRule(localctx, 32, self.RULE_oC_SetItem) + self._la = 0 # Token type + try: + self.state = 441 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,48,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 405 + self.oC_PropertyExpression() + self.state = 407 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 406 + self.match(CypherParser.SP) + + + self.state = 409 + self.match(CypherParser.T__2) + self.state = 411 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 410 + self.match(CypherParser.SP) + + + self.state = 413 + self.oC_Expression() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 415 + self.oC_Variable() + self.state = 417 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 416 + self.match(CypherParser.SP) + + + self.state = 419 + self.match(CypherParser.T__2) + self.state = 421 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 420 + self.match(CypherParser.SP) + + + self.state = 423 + self.oC_Expression() + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 425 + self.oC_Variable() + self.state = 427 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 426 + self.match(CypherParser.SP) + + + self.state = 429 + self.match(CypherParser.T__3) + self.state = 431 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 430 + self.match(CypherParser.SP) + + + self.state = 433 + self.oC_Expression() + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 435 + self.oC_Variable() + self.state = 437 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 436 + self.match(CypherParser.SP) + + + self.state = 439 + self.oC_NodeLabels() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_DeleteContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def DELETE(self): + return self.getToken(CypherParser.DELETE, 0) + + def oC_Expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,i) + + + def DETACH(self): + return self.getToken(CypherParser.DETACH, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_Delete + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Delete" ): + listener.enterOC_Delete(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Delete" ): + listener.exitOC_Delete(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Delete" ): + return visitor.visitOC_Delete(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Delete(self): + + localctx = CypherParser.OC_DeleteContext(self, self._ctx, self.state) + self.enterRule(localctx, 34, self.RULE_oC_Delete) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 445 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==56: + self.state = 443 + self.match(CypherParser.DETACH) + self.state = 444 + self.match(CypherParser.SP) + + + self.state = 447 + self.match(CypherParser.DELETE) + self.state = 449 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 448 + self.match(CypherParser.SP) + + + self.state = 451 + self.oC_Expression() + self.state = 462 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,53,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 453 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 452 + self.match(CypherParser.SP) + + + self.state = 455 + self.match(CypherParser.T__1) + self.state = 457 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 456 + self.match(CypherParser.SP) + + + self.state = 459 + self.oC_Expression() + self.state = 464 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,53,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_RemoveContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def REMOVE(self): + return self.getToken(CypherParser.REMOVE, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_RemoveItem(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_RemoveItemContext) + else: + return self.getTypedRuleContext(CypherParser.OC_RemoveItemContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Remove + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Remove" ): + listener.enterOC_Remove(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Remove" ): + listener.exitOC_Remove(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Remove" ): + return visitor.visitOC_Remove(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Remove(self): + + localctx = CypherParser.OC_RemoveContext(self, self._ctx, self.state) + self.enterRule(localctx, 36, self.RULE_oC_Remove) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 465 + self.match(CypherParser.REMOVE) + self.state = 466 + self.match(CypherParser.SP) + self.state = 467 + self.oC_RemoveItem() + self.state = 478 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,56,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 469 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 468 + self.match(CypherParser.SP) + + + self.state = 471 + self.match(CypherParser.T__1) + self.state = 473 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 472 + self.match(CypherParser.SP) + + + self.state = 475 + self.oC_RemoveItem() + self.state = 480 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,56,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_RemoveItemContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def oC_NodeLabels(self): + return self.getTypedRuleContext(CypherParser.OC_NodeLabelsContext,0) + + + def oC_PropertyExpression(self): + return self.getTypedRuleContext(CypherParser.OC_PropertyExpressionContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_RemoveItem + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_RemoveItem" ): + listener.enterOC_RemoveItem(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_RemoveItem" ): + listener.exitOC_RemoveItem(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_RemoveItem" ): + return visitor.visitOC_RemoveItem(self) + else: + return visitor.visitChildren(self) + + + + + def oC_RemoveItem(self): + + localctx = CypherParser.OC_RemoveItemContext(self, self._ctx, self.state) + self.enterRule(localctx, 38, self.RULE_oC_RemoveItem) + try: + self.state = 485 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,57,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 481 + self.oC_Variable() + self.state = 482 + self.oC_NodeLabels() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 484 + self.oC_PropertyExpression() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_InQueryCallContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def CALL(self): + return self.getToken(CypherParser.CALL, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_ExplicitProcedureInvocation(self): + return self.getTypedRuleContext(CypherParser.OC_ExplicitProcedureInvocationContext,0) + + + def YIELD(self): + return self.getToken(CypherParser.YIELD, 0) + + def oC_YieldItems(self): + return self.getTypedRuleContext(CypherParser.OC_YieldItemsContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_InQueryCall + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_InQueryCall" ): + listener.enterOC_InQueryCall(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_InQueryCall" ): + listener.exitOC_InQueryCall(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_InQueryCall" ): + return visitor.visitOC_InQueryCall(self) + else: + return visitor.visitChildren(self) + + + + + def oC_InQueryCall(self): + + localctx = CypherParser.OC_InQueryCallContext(self, self._ctx, self.state) + self.enterRule(localctx, 40, self.RULE_oC_InQueryCall) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 487 + self.match(CypherParser.CALL) + self.state = 488 + self.match(CypherParser.SP) + self.state = 489 + self.oC_ExplicitProcedureInvocation() + self.state = 496 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,59,self._ctx) + if la_ == 1: + self.state = 491 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 490 + self.match(CypherParser.SP) + + + self.state = 493 + self.match(CypherParser.YIELD) + self.state = 494 + self.match(CypherParser.SP) + self.state = 495 + self.oC_YieldItems() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_StandaloneCallContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def CALL(self): + return self.getToken(CypherParser.CALL, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_ExplicitProcedureInvocation(self): + return self.getTypedRuleContext(CypherParser.OC_ExplicitProcedureInvocationContext,0) + + + def oC_ImplicitProcedureInvocation(self): + return self.getTypedRuleContext(CypherParser.OC_ImplicitProcedureInvocationContext,0) + + + def YIELD(self): + return self.getToken(CypherParser.YIELD, 0) + + def oC_YieldItems(self): + return self.getTypedRuleContext(CypherParser.OC_YieldItemsContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_StandaloneCall + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_StandaloneCall" ): + listener.enterOC_StandaloneCall(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_StandaloneCall" ): + listener.exitOC_StandaloneCall(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_StandaloneCall" ): + return visitor.visitOC_StandaloneCall(self) + else: + return visitor.visitChildren(self) + + + + + def oC_StandaloneCall(self): + + localctx = CypherParser.OC_StandaloneCallContext(self, self._ctx, self.state) + self.enterRule(localctx, 42, self.RULE_oC_StandaloneCall) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 498 + self.match(CypherParser.CALL) + self.state = 499 + self.match(CypherParser.SP) + self.state = 502 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,60,self._ctx) + if la_ == 1: + self.state = 500 + self.oC_ExplicitProcedureInvocation() + pass + + elif la_ == 2: + self.state = 501 + self.oC_ImplicitProcedureInvocation() + pass + + + self.state = 513 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,63,self._ctx) + if la_ == 1: + self.state = 505 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 504 + self.match(CypherParser.SP) + + + self.state = 507 + self.match(CypherParser.YIELD) + self.state = 508 + self.match(CypherParser.SP) + self.state = 511 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [5]: + self.state = 509 + self.match(CypherParser.T__4) + pass + elif token in [83, 89, 90, 91, 98, 119, 120, 121, 124]: + self.state = 510 + self.oC_YieldItems() + pass + else: + raise NoViableAltException(self) + + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_YieldItemsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_YieldItem(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_YieldItemContext) + else: + return self.getTypedRuleContext(CypherParser.OC_YieldItemContext,i) + + + def oC_Where(self): + return self.getTypedRuleContext(CypherParser.OC_WhereContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_YieldItems + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_YieldItems" ): + listener.enterOC_YieldItems(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_YieldItems" ): + listener.exitOC_YieldItems(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_YieldItems" ): + return visitor.visitOC_YieldItems(self) + else: + return visitor.visitChildren(self) + + + + + def oC_YieldItems(self): + + localctx = CypherParser.OC_YieldItemsContext(self, self._ctx, self.state) + self.enterRule(localctx, 44, self.RULE_oC_YieldItems) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 515 + self.oC_YieldItem() + self.state = 526 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,66,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 517 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 516 + self.match(CypherParser.SP) + + + self.state = 519 + self.match(CypherParser.T__1) + self.state = 521 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 520 + self.match(CypherParser.SP) + + + self.state = 523 + self.oC_YieldItem() + self.state = 528 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,66,self._ctx) + + self.state = 533 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,68,self._ctx) + if la_ == 1: + self.state = 530 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 529 + self.match(CypherParser.SP) + + + self.state = 532 + self.oC_Where() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_YieldItemContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def oC_ProcedureResultField(self): + return self.getTypedRuleContext(CypherParser.OC_ProcedureResultFieldContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def AS(self): + return self.getToken(CypherParser.AS, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_YieldItem + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_YieldItem" ): + listener.enterOC_YieldItem(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_YieldItem" ): + listener.exitOC_YieldItem(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_YieldItem" ): + return visitor.visitOC_YieldItem(self) + else: + return visitor.visitChildren(self) + + + + + def oC_YieldItem(self): + + localctx = CypherParser.OC_YieldItemContext(self, self._ctx, self.state) + self.enterRule(localctx, 46, self.RULE_oC_YieldItem) + try: + self.enterOuterAlt(localctx, 1) + self.state = 540 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,69,self._ctx) + if la_ == 1: + self.state = 535 + self.oC_ProcedureResultField() + self.state = 536 + self.match(CypherParser.SP) + self.state = 537 + self.match(CypherParser.AS) + self.state = 538 + self.match(CypherParser.SP) + + + self.state = 542 + self.oC_Variable() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_WithContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def WITH(self): + return self.getToken(CypherParser.WITH, 0) + + def oC_ProjectionBody(self): + return self.getTypedRuleContext(CypherParser.OC_ProjectionBodyContext,0) + + + def oC_Where(self): + return self.getTypedRuleContext(CypherParser.OC_WhereContext,0) + + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_With + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_With" ): + listener.enterOC_With(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_With" ): + listener.exitOC_With(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_With" ): + return visitor.visitOC_With(self) + else: + return visitor.visitChildren(self) + + + + + def oC_With(self): + + localctx = CypherParser.OC_WithContext(self, self._ctx, self.state) + self.enterRule(localctx, 48, self.RULE_oC_With) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 544 + self.match(CypherParser.WITH) + self.state = 545 + self.oC_ProjectionBody() + self.state = 550 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,71,self._ctx) + if la_ == 1: + self.state = 547 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 546 + self.match(CypherParser.SP) + + + self.state = 549 + self.oC_Where() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ReturnContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def RETURN(self): + return self.getToken(CypherParser.RETURN, 0) + + def oC_ProjectionBody(self): + return self.getTypedRuleContext(CypherParser.OC_ProjectionBodyContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Return + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Return" ): + listener.enterOC_Return(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Return" ): + listener.exitOC_Return(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Return" ): + return visitor.visitOC_Return(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Return(self): + + localctx = CypherParser.OC_ReturnContext(self, self._ctx, self.state) + self.enterRule(localctx, 50, self.RULE_oC_Return) + try: + self.enterOuterAlt(localctx, 1) + self.state = 552 + self.match(CypherParser.RETURN) + self.state = 553 + self.oC_ProjectionBody() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ProjectionBodyContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_ProjectionItems(self): + return self.getTypedRuleContext(CypherParser.OC_ProjectionItemsContext,0) + + + def DISTINCT(self): + return self.getToken(CypherParser.DISTINCT, 0) + + def oC_Order(self): + return self.getTypedRuleContext(CypherParser.OC_OrderContext,0) + + + def oC_Skip(self): + return self.getTypedRuleContext(CypherParser.OC_SkipContext,0) + + + def oC_Limit(self): + return self.getTypedRuleContext(CypherParser.OC_LimitContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ProjectionBody + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ProjectionBody" ): + listener.enterOC_ProjectionBody(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ProjectionBody" ): + listener.exitOC_ProjectionBody(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ProjectionBody" ): + return visitor.visitOC_ProjectionBody(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ProjectionBody(self): + + localctx = CypherParser.OC_ProjectionBodyContext(self, self._ctx, self.state) + self.enterRule(localctx, 52, self.RULE_oC_ProjectionBody) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 559 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,73,self._ctx) + if la_ == 1: + self.state = 556 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 555 + self.match(CypherParser.SP) + + + self.state = 558 + self.match(CypherParser.DISTINCT) + + + self.state = 561 + self.match(CypherParser.SP) + self.state = 562 + self.oC_ProjectionItems() + self.state = 565 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,74,self._ctx) + if la_ == 1: + self.state = 563 + self.match(CypherParser.SP) + self.state = 564 + self.oC_Order() + + + self.state = 569 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,75,self._ctx) + if la_ == 1: + self.state = 567 + self.match(CypherParser.SP) + self.state = 568 + self.oC_Skip() + + + self.state = 573 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,76,self._ctx) + if la_ == 1: + self.state = 571 + self.match(CypherParser.SP) + self.state = 572 + self.oC_Limit() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ProjectionItemsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_ProjectionItem(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ProjectionItemContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ProjectionItemContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_ProjectionItems + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ProjectionItems" ): + listener.enterOC_ProjectionItems(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ProjectionItems" ): + listener.exitOC_ProjectionItems(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ProjectionItems" ): + return visitor.visitOC_ProjectionItems(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ProjectionItems(self): + + localctx = CypherParser.OC_ProjectionItemsContext(self, self._ctx, self.state) + self.enterRule(localctx, 54, self.RULE_oC_ProjectionItems) + self._la = 0 # Token type + try: + self.state = 603 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [5]: + self.enterOuterAlt(localctx, 1) + self.state = 575 + self.match(CypherParser.T__4) + self.state = 586 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,79,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 577 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 576 + self.match(CypherParser.SP) + + + self.state = 579 + self.match(CypherParser.T__1) + self.state = 581 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 580 + self.match(CypherParser.SP) + + + self.state = 583 + self.oC_ProjectionItem() + self.state = 588 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,79,self._ctx) + + pass + elif token in [6, 8, 18, 19, 24, 26, 47, 76, 82, 83, 84, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 105, 106, 107, 119, 120, 121, 124]: + self.enterOuterAlt(localctx, 2) + self.state = 589 + self.oC_ProjectionItem() + self.state = 600 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,82,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 591 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 590 + self.match(CypherParser.SP) + + + self.state = 593 + self.match(CypherParser.T__1) + self.state = 595 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 594 + self.match(CypherParser.SP) + + + self.state = 597 + self.oC_ProjectionItem() + self.state = 602 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,82,self._ctx) + + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ProjectionItemContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def AS(self): + return self.getToken(CypherParser.AS, 0) + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ProjectionItem + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ProjectionItem" ): + listener.enterOC_ProjectionItem(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ProjectionItem" ): + listener.exitOC_ProjectionItem(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ProjectionItem" ): + return visitor.visitOC_ProjectionItem(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ProjectionItem(self): + + localctx = CypherParser.OC_ProjectionItemContext(self, self._ctx, self.state) + self.enterRule(localctx, 56, self.RULE_oC_ProjectionItem) + try: + self.state = 612 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,84,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 605 + self.oC_Expression() + self.state = 606 + self.match(CypherParser.SP) + self.state = 607 + self.match(CypherParser.AS) + self.state = 608 + self.match(CypherParser.SP) + self.state = 609 + self.oC_Variable() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 611 + self.oC_Expression() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_OrderContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ORDER(self): + return self.getToken(CypherParser.ORDER, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def BY(self): + return self.getToken(CypherParser.BY, 0) + + def oC_SortItem(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_SortItemContext) + else: + return self.getTypedRuleContext(CypherParser.OC_SortItemContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Order + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Order" ): + listener.enterOC_Order(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Order" ): + listener.exitOC_Order(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Order" ): + return visitor.visitOC_Order(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Order(self): + + localctx = CypherParser.OC_OrderContext(self, self._ctx, self.state) + self.enterRule(localctx, 58, self.RULE_oC_Order) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 614 + self.match(CypherParser.ORDER) + self.state = 615 + self.match(CypherParser.SP) + self.state = 616 + self.match(CypherParser.BY) + self.state = 617 + self.match(CypherParser.SP) + self.state = 618 + self.oC_SortItem() + self.state = 626 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==2: + self.state = 619 + self.match(CypherParser.T__1) + self.state = 621 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 620 + self.match(CypherParser.SP) + + + self.state = 623 + self.oC_SortItem() + self.state = 628 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_SkipContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def L_SKIP(self): + return self.getToken(CypherParser.L_SKIP, 0) + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Skip + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Skip" ): + listener.enterOC_Skip(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Skip" ): + listener.exitOC_Skip(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Skip" ): + return visitor.visitOC_Skip(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Skip(self): + + localctx = CypherParser.OC_SkipContext(self, self._ctx, self.state) + self.enterRule(localctx, 60, self.RULE_oC_Skip) + try: + self.enterOuterAlt(localctx, 1) + self.state = 629 + self.match(CypherParser.L_SKIP) + self.state = 630 + self.match(CypherParser.SP) + self.state = 631 + self.oC_Expression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_LimitContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LIMIT(self): + return self.getToken(CypherParser.LIMIT, 0) + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Limit + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Limit" ): + listener.enterOC_Limit(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Limit" ): + listener.exitOC_Limit(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Limit" ): + return visitor.visitOC_Limit(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Limit(self): + + localctx = CypherParser.OC_LimitContext(self, self._ctx, self.state) + self.enterRule(localctx, 62, self.RULE_oC_Limit) + try: + self.enterOuterAlt(localctx, 1) + self.state = 633 + self.match(CypherParser.LIMIT) + self.state = 634 + self.match(CypherParser.SP) + self.state = 635 + self.oC_Expression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_SortItemContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def ASCENDING(self): + return self.getToken(CypherParser.ASCENDING, 0) + + def ASC(self): + return self.getToken(CypherParser.ASC, 0) + + def DESCENDING(self): + return self.getToken(CypherParser.DESCENDING, 0) + + def DESC(self): + return self.getToken(CypherParser.DESC, 0) + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_SortItem + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_SortItem" ): + listener.enterOC_SortItem(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_SortItem" ): + listener.exitOC_SortItem(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_SortItem" ): + return visitor.visitOC_SortItem(self) + else: + return visitor.visitChildren(self) + + + + + def oC_SortItem(self): + + localctx = CypherParser.OC_SortItemContext(self, self._ctx, self.state) + self.enterRule(localctx, 64, self.RULE_oC_SortItem) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 637 + self.oC_Expression() + self.state = 642 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,88,self._ctx) + if la_ == 1: + self.state = 639 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 638 + self.match(CypherParser.SP) + + + self.state = 641 + _la = self._input.LA(1) + if not(((((_la - 68)) & ~0x3f) == 0 and ((1 << (_la - 68)) & 15) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_WhereContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def WHERE(self): + return self.getToken(CypherParser.WHERE, 0) + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Where + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Where" ): + listener.enterOC_Where(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Where" ): + listener.exitOC_Where(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Where" ): + return visitor.visitOC_Where(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Where(self): + + localctx = CypherParser.OC_WhereContext(self, self._ctx, self.state) + self.enterRule(localctx, 66, self.RULE_oC_Where) + try: + self.enterOuterAlt(localctx, 1) + self.state = 644 + self.match(CypherParser.WHERE) + self.state = 645 + self.match(CypherParser.SP) + self.state = 646 + self.oC_Expression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PatternContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_PatternPart(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_PatternPartContext) + else: + return self.getTypedRuleContext(CypherParser.OC_PatternPartContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_Pattern + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Pattern" ): + listener.enterOC_Pattern(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Pattern" ): + listener.exitOC_Pattern(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Pattern" ): + return visitor.visitOC_Pattern(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Pattern(self): + + localctx = CypherParser.OC_PatternContext(self, self._ctx, self.state) + self.enterRule(localctx, 68, self.RULE_oC_Pattern) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 648 + self.oC_PatternPart() + self.state = 659 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,91,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 650 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 649 + self.match(CypherParser.SP) + + + self.state = 652 + self.match(CypherParser.T__1) + self.state = 654 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 653 + self.match(CypherParser.SP) + + + self.state = 656 + self.oC_PatternPart() + self.state = 661 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,91,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PatternPartContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def oC_AnonymousPatternPart(self): + return self.getTypedRuleContext(CypherParser.OC_AnonymousPatternPartContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_PatternPart + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_PatternPart" ): + listener.enterOC_PatternPart(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_PatternPart" ): + listener.exitOC_PatternPart(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_PatternPart" ): + return visitor.visitOC_PatternPart(self) + else: + return visitor.visitChildren(self) + + + + + def oC_PatternPart(self): + + localctx = CypherParser.OC_PatternPartContext(self, self._ctx, self.state) + self.enterRule(localctx, 70, self.RULE_oC_PatternPart) + self._la = 0 # Token type + try: + self.state = 673 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [83, 89, 90, 91, 98, 119, 120, 121, 124]: + self.enterOuterAlt(localctx, 1) + self.state = 662 + self.oC_Variable() + self.state = 664 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 663 + self.match(CypherParser.SP) + + + self.state = 666 + self.match(CypherParser.T__2) + self.state = 668 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 667 + self.match(CypherParser.SP) + + + self.state = 670 + self.oC_AnonymousPatternPart() + pass + elif token in [6]: + self.enterOuterAlt(localctx, 2) + self.state = 672 + self.oC_AnonymousPatternPart() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_AnonymousPatternPartContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_PatternElement(self): + return self.getTypedRuleContext(CypherParser.OC_PatternElementContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_AnonymousPatternPart + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_AnonymousPatternPart" ): + listener.enterOC_AnonymousPatternPart(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_AnonymousPatternPart" ): + listener.exitOC_AnonymousPatternPart(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_AnonymousPatternPart" ): + return visitor.visitOC_AnonymousPatternPart(self) + else: + return visitor.visitChildren(self) + + + + + def oC_AnonymousPatternPart(self): + + localctx = CypherParser.OC_AnonymousPatternPartContext(self, self._ctx, self.state) + self.enterRule(localctx, 72, self.RULE_oC_AnonymousPatternPart) + try: + self.enterOuterAlt(localctx, 1) + self.state = 675 + self.oC_PatternElement() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PatternElementContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_NodePattern(self): + return self.getTypedRuleContext(CypherParser.OC_NodePatternContext,0) + + + def oC_PatternElementChain(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_PatternElementChainContext) + else: + return self.getTypedRuleContext(CypherParser.OC_PatternElementChainContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_PatternElement(self): + return self.getTypedRuleContext(CypherParser.OC_PatternElementContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_PatternElement + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_PatternElement" ): + listener.enterOC_PatternElement(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_PatternElement" ): + listener.exitOC_PatternElement(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_PatternElement" ): + return visitor.visitOC_PatternElement(self) + else: + return visitor.visitChildren(self) + + + + + def oC_PatternElement(self): + + localctx = CypherParser.OC_PatternElementContext(self, self._ctx, self.state) + self.enterRule(localctx, 74, self.RULE_oC_PatternElement) + self._la = 0 # Token type + try: + self.state = 691 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,97,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 677 + self.oC_NodePattern() + self.state = 684 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,96,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 679 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 678 + self.match(CypherParser.SP) + + + self.state = 681 + self.oC_PatternElementChain() + self.state = 686 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,96,self._ctx) + + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 687 + self.match(CypherParser.T__5) + self.state = 688 + self.oC_PatternElement() + self.state = 689 + self.match(CypherParser.T__6) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_RelationshipsPatternContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_NodePattern(self): + return self.getTypedRuleContext(CypherParser.OC_NodePatternContext,0) + + + def oC_PatternElementChain(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_PatternElementChainContext) + else: + return self.getTypedRuleContext(CypherParser.OC_PatternElementChainContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_RelationshipsPattern + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_RelationshipsPattern" ): + listener.enterOC_RelationshipsPattern(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_RelationshipsPattern" ): + listener.exitOC_RelationshipsPattern(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_RelationshipsPattern" ): + return visitor.visitOC_RelationshipsPattern(self) + else: + return visitor.visitChildren(self) + + + + + def oC_RelationshipsPattern(self): + + localctx = CypherParser.OC_RelationshipsPatternContext(self, self._ctx, self.state) + self.enterRule(localctx, 76, self.RULE_oC_RelationshipsPattern) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 693 + self.oC_NodePattern() + self.state = 698 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 695 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 694 + self.match(CypherParser.SP) + + + self.state = 697 + self.oC_PatternElementChain() + + else: + raise NoViableAltException(self) + self.state = 700 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,99,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_NodePatternContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def oC_NodeLabels(self): + return self.getTypedRuleContext(CypherParser.OC_NodeLabelsContext,0) + + + def oC_Properties(self): + return self.getTypedRuleContext(CypherParser.OC_PropertiesContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_NodePattern + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_NodePattern" ): + listener.enterOC_NodePattern(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_NodePattern" ): + listener.exitOC_NodePattern(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_NodePattern" ): + return visitor.visitOC_NodePattern(self) + else: + return visitor.visitChildren(self) + + + + + def oC_NodePattern(self): + + localctx = CypherParser.OC_NodePatternContext(self, self._ctx, self.state) + self.enterRule(localctx, 78, self.RULE_oC_NodePattern) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 702 + self.match(CypherParser.T__5) + self.state = 704 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 703 + self.match(CypherParser.SP) + + + self.state = 710 + self._errHandler.sync(self) + _la = self._input.LA(1) + if ((((_la - 83)) & ~0x3f) == 0 and ((1 << (_la - 83)) & 2680059625921) != 0): + self.state = 706 + self.oC_Variable() + self.state = 708 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 707 + self.match(CypherParser.SP) + + + + + self.state = 716 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==10: + self.state = 712 + self.oC_NodeLabels() + self.state = 714 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 713 + self.match(CypherParser.SP) + + + + + self.state = 722 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==24 or _la==26: + self.state = 718 + self.oC_Properties() + self.state = 720 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 719 + self.match(CypherParser.SP) + + + + + self.state = 724 + self.match(CypherParser.T__6) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PatternElementChainContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_RelationshipPattern(self): + return self.getTypedRuleContext(CypherParser.OC_RelationshipPatternContext,0) + + + def oC_NodePattern(self): + return self.getTypedRuleContext(CypherParser.OC_NodePatternContext,0) + + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_PatternElementChain + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_PatternElementChain" ): + listener.enterOC_PatternElementChain(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_PatternElementChain" ): + listener.exitOC_PatternElementChain(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_PatternElementChain" ): + return visitor.visitOC_PatternElementChain(self) + else: + return visitor.visitChildren(self) + + + + + def oC_PatternElementChain(self): + + localctx = CypherParser.OC_PatternElementChainContext(self, self._ctx, self.state) + self.enterRule(localctx, 80, self.RULE_oC_PatternElementChain) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 726 + self.oC_RelationshipPattern() + self.state = 728 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 727 + self.match(CypherParser.SP) + + + self.state = 730 + self.oC_NodePattern() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_RelationshipPatternContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_LeftArrowHead(self): + return self.getTypedRuleContext(CypherParser.OC_LeftArrowHeadContext,0) + + + def oC_Dash(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_DashContext) + else: + return self.getTypedRuleContext(CypherParser.OC_DashContext,i) + + + def oC_RightArrowHead(self): + return self.getTypedRuleContext(CypherParser.OC_RightArrowHeadContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_RelationshipDetail(self): + return self.getTypedRuleContext(CypherParser.OC_RelationshipDetailContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_RelationshipPattern + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_RelationshipPattern" ): + listener.enterOC_RelationshipPattern(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_RelationshipPattern" ): + listener.exitOC_RelationshipPattern(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_RelationshipPattern" ): + return visitor.visitOC_RelationshipPattern(self) + else: + return visitor.visitChildren(self) + + + + + def oC_RelationshipPattern(self): + + localctx = CypherParser.OC_RelationshipPatternContext(self, self._ctx, self.state) + self.enterRule(localctx, 82, self.RULE_oC_RelationshipPattern) + self._la = 0 # Token type + try: + self.state = 796 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,124,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 732 + self.oC_LeftArrowHead() + self.state = 734 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 733 + self.match(CypherParser.SP) + + + self.state = 736 + self.oC_Dash() + self.state = 738 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,109,self._ctx) + if la_ == 1: + self.state = 737 + self.match(CypherParser.SP) + + + self.state = 741 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==8: + self.state = 740 + self.oC_RelationshipDetail() + + + self.state = 744 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 743 + self.match(CypherParser.SP) + + + self.state = 746 + self.oC_Dash() + self.state = 748 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 747 + self.match(CypherParser.SP) + + + self.state = 750 + self.oC_RightArrowHead() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 752 + self.oC_LeftArrowHead() + self.state = 754 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 753 + self.match(CypherParser.SP) + + + self.state = 756 + self.oC_Dash() + self.state = 758 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,114,self._ctx) + if la_ == 1: + self.state = 757 + self.match(CypherParser.SP) + + + self.state = 761 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==8: + self.state = 760 + self.oC_RelationshipDetail() + + + self.state = 764 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 763 + self.match(CypherParser.SP) + + + self.state = 766 + self.oC_Dash() + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 768 + self.oC_Dash() + self.state = 770 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,117,self._ctx) + if la_ == 1: + self.state = 769 + self.match(CypherParser.SP) + + + self.state = 773 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==8: + self.state = 772 + self.oC_RelationshipDetail() + + + self.state = 776 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 775 + self.match(CypherParser.SP) + + + self.state = 778 + self.oC_Dash() + self.state = 780 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 779 + self.match(CypherParser.SP) + + + self.state = 782 + self.oC_RightArrowHead() + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 784 + self.oC_Dash() + self.state = 786 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,121,self._ctx) + if la_ == 1: + self.state = 785 + self.match(CypherParser.SP) + + + self.state = 789 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==8: + self.state = 788 + self.oC_RelationshipDetail() + + + self.state = 792 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 791 + self.match(CypherParser.SP) + + + self.state = 794 + self.oC_Dash() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_RelationshipDetailContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def oC_RelationshipTypes(self): + return self.getTypedRuleContext(CypherParser.OC_RelationshipTypesContext,0) + + + def oC_RangeLiteral(self): + return self.getTypedRuleContext(CypherParser.OC_RangeLiteralContext,0) + + + def oC_Properties(self): + return self.getTypedRuleContext(CypherParser.OC_PropertiesContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_RelationshipDetail + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_RelationshipDetail" ): + listener.enterOC_RelationshipDetail(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_RelationshipDetail" ): + listener.exitOC_RelationshipDetail(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_RelationshipDetail" ): + return visitor.visitOC_RelationshipDetail(self) + else: + return visitor.visitChildren(self) + + + + + def oC_RelationshipDetail(self): + + localctx = CypherParser.OC_RelationshipDetailContext(self, self._ctx, self.state) + self.enterRule(localctx, 84, self.RULE_oC_RelationshipDetail) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 798 + self.match(CypherParser.T__7) + self.state = 800 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 799 + self.match(CypherParser.SP) + + + self.state = 806 + self._errHandler.sync(self) + _la = self._input.LA(1) + if ((((_la - 83)) & ~0x3f) == 0 and ((1 << (_la - 83)) & 2680059625921) != 0): + self.state = 802 + self.oC_Variable() + self.state = 804 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 803 + self.match(CypherParser.SP) + + + + + self.state = 812 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==10: + self.state = 808 + self.oC_RelationshipTypes() + self.state = 810 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 809 + self.match(CypherParser.SP) + + + + + self.state = 815 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==5: + self.state = 814 + self.oC_RangeLiteral() + + + self.state = 821 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==24 or _la==26: + self.state = 817 + self.oC_Properties() + self.state = 819 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 818 + self.match(CypherParser.SP) + + + + + self.state = 823 + self.match(CypherParser.T__8) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PropertiesContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_MapLiteral(self): + return self.getTypedRuleContext(CypherParser.OC_MapLiteralContext,0) + + + def oC_Parameter(self): + return self.getTypedRuleContext(CypherParser.OC_ParameterContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Properties + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Properties" ): + listener.enterOC_Properties(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Properties" ): + listener.exitOC_Properties(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Properties" ): + return visitor.visitOC_Properties(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Properties(self): + + localctx = CypherParser.OC_PropertiesContext(self, self._ctx, self.state) + self.enterRule(localctx, 86, self.RULE_oC_Properties) + try: + self.state = 827 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [24]: + self.enterOuterAlt(localctx, 1) + self.state = 825 + self.oC_MapLiteral() + pass + elif token in [26]: + self.enterOuterAlt(localctx, 2) + self.state = 826 + self.oC_Parameter() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_RelationshipTypesContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_RelTypeName(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_RelTypeNameContext) + else: + return self.getTypedRuleContext(CypherParser.OC_RelTypeNameContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_RelationshipTypes + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_RelationshipTypes" ): + listener.enterOC_RelationshipTypes(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_RelationshipTypes" ): + listener.exitOC_RelationshipTypes(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_RelationshipTypes" ): + return visitor.visitOC_RelationshipTypes(self) + else: + return visitor.visitChildren(self) + + + + + def oC_RelationshipTypes(self): + + localctx = CypherParser.OC_RelationshipTypesContext(self, self._ctx, self.state) + self.enterRule(localctx, 88, self.RULE_oC_RelationshipTypes) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 829 + self.match(CypherParser.T__9) + self.state = 831 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 830 + self.match(CypherParser.SP) + + + self.state = 833 + self.oC_RelTypeName() + self.state = 847 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,138,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 835 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 834 + self.match(CypherParser.SP) + + + self.state = 837 + self.match(CypherParser.T__10) + self.state = 839 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==10: + self.state = 838 + self.match(CypherParser.T__9) + + + self.state = 842 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 841 + self.match(CypherParser.SP) + + + self.state = 844 + self.oC_RelTypeName() + self.state = 849 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,138,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_NodeLabelsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_NodeLabel(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_NodeLabelContext) + else: + return self.getTypedRuleContext(CypherParser.OC_NodeLabelContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_NodeLabels + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_NodeLabels" ): + listener.enterOC_NodeLabels(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_NodeLabels" ): + listener.exitOC_NodeLabels(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_NodeLabels" ): + return visitor.visitOC_NodeLabels(self) + else: + return visitor.visitChildren(self) + + + + + def oC_NodeLabels(self): + + localctx = CypherParser.OC_NodeLabelsContext(self, self._ctx, self.state) + self.enterRule(localctx, 90, self.RULE_oC_NodeLabels) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 850 + self.oC_NodeLabel() + self.state = 857 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,140,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 852 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 851 + self.match(CypherParser.SP) + + + self.state = 854 + self.oC_NodeLabel() + self.state = 859 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,140,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_NodeLabelContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_LabelName(self): + return self.getTypedRuleContext(CypherParser.OC_LabelNameContext,0) + + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_NodeLabel + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_NodeLabel" ): + listener.enterOC_NodeLabel(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_NodeLabel" ): + listener.exitOC_NodeLabel(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_NodeLabel" ): + return visitor.visitOC_NodeLabel(self) + else: + return visitor.visitChildren(self) + + + + + def oC_NodeLabel(self): + + localctx = CypherParser.OC_NodeLabelContext(self, self._ctx, self.state) + self.enterRule(localctx, 92, self.RULE_oC_NodeLabel) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 860 + self.match(CypherParser.T__9) + self.state = 862 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 861 + self.match(CypherParser.SP) + + + self.state = 864 + self.oC_LabelName() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_RangeLiteralContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_IntegerLiteral(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_IntegerLiteralContext) + else: + return self.getTypedRuleContext(CypherParser.OC_IntegerLiteralContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_RangeLiteral + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_RangeLiteral" ): + listener.enterOC_RangeLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_RangeLiteral" ): + listener.exitOC_RangeLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_RangeLiteral" ): + return visitor.visitOC_RangeLiteral(self) + else: + return visitor.visitChildren(self) + + + + + def oC_RangeLiteral(self): + + localctx = CypherParser.OC_RangeLiteralContext(self, self._ctx, self.state) + self.enterRule(localctx, 94, self.RULE_oC_RangeLiteral) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 866 + self.match(CypherParser.T__4) + self.state = 868 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 867 + self.match(CypherParser.SP) + + + self.state = 874 + self._errHandler.sync(self) + _la = self._input.LA(1) + if ((((_la - 95)) & ~0x3f) == 0 and ((1 << (_la - 95)) & 7) != 0): + self.state = 870 + self.oC_IntegerLiteral() + self.state = 872 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 871 + self.match(CypherParser.SP) + + + + + self.state = 886 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==12: + self.state = 876 + self.match(CypherParser.T__11) + self.state = 878 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 877 + self.match(CypherParser.SP) + + + self.state = 884 + self._errHandler.sync(self) + _la = self._input.LA(1) + if ((((_la - 95)) & ~0x3f) == 0 and ((1 << (_la - 95)) & 7) != 0): + self.state = 880 + self.oC_IntegerLiteral() + self.state = 882 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 881 + self.match(CypherParser.SP) + + + + + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_LabelNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SchemaName(self): + return self.getTypedRuleContext(CypherParser.OC_SchemaNameContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_LabelName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_LabelName" ): + listener.enterOC_LabelName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_LabelName" ): + listener.exitOC_LabelName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_LabelName" ): + return visitor.visitOC_LabelName(self) + else: + return visitor.visitChildren(self) + + + + + def oC_LabelName(self): + + localctx = CypherParser.OC_LabelNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 96, self.RULE_oC_LabelName) + try: + self.enterOuterAlt(localctx, 1) + self.state = 888 + self.oC_SchemaName() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_RelTypeNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SchemaName(self): + return self.getTypedRuleContext(CypherParser.OC_SchemaNameContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_RelTypeName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_RelTypeName" ): + listener.enterOC_RelTypeName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_RelTypeName" ): + listener.exitOC_RelTypeName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_RelTypeName" ): + return visitor.visitOC_RelTypeName(self) + else: + return visitor.visitChildren(self) + + + + + def oC_RelTypeName(self): + + localctx = CypherParser.OC_RelTypeNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 98, self.RULE_oC_RelTypeName) + try: + self.enterOuterAlt(localctx, 1) + self.state = 890 + self.oC_SchemaName() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PropertyExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Atom(self): + return self.getTypedRuleContext(CypherParser.OC_AtomContext,0) + + + def oC_PropertyLookup(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_PropertyLookupContext) + else: + return self.getTypedRuleContext(CypherParser.OC_PropertyLookupContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_PropertyExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_PropertyExpression" ): + listener.enterOC_PropertyExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_PropertyExpression" ): + listener.exitOC_PropertyExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_PropertyExpression" ): + return visitor.visitOC_PropertyExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_PropertyExpression(self): + + localctx = CypherParser.OC_PropertyExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 100, self.RULE_oC_PropertyExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 892 + self.oC_Atom() + self.state = 897 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 894 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 893 + self.match(CypherParser.SP) + + + self.state = 896 + self.oC_PropertyLookup() + + else: + raise NoViableAltException(self) + self.state = 899 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,150,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_OrExpression(self): + return self.getTypedRuleContext(CypherParser.OC_OrExpressionContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Expression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Expression" ): + listener.enterOC_Expression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Expression" ): + listener.exitOC_Expression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Expression" ): + return visitor.visitOC_Expression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Expression(self): + + localctx = CypherParser.OC_ExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 102, self.RULE_oC_Expression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 901 + self.oC_OrExpression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_OrExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_XorExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_XorExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_XorExpressionContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def OR(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.OR) + else: + return self.getToken(CypherParser.OR, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_OrExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_OrExpression" ): + listener.enterOC_OrExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_OrExpression" ): + listener.exitOC_OrExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_OrExpression" ): + return visitor.visitOC_OrExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_OrExpression(self): + + localctx = CypherParser.OC_OrExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 104, self.RULE_oC_OrExpression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 903 + self.oC_XorExpression() + self.state = 910 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,151,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 904 + self.match(CypherParser.SP) + self.state = 905 + self.match(CypherParser.OR) + self.state = 906 + self.match(CypherParser.SP) + self.state = 907 + self.oC_XorExpression() + self.state = 912 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,151,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_XorExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_AndExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_AndExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_AndExpressionContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def XOR(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.XOR) + else: + return self.getToken(CypherParser.XOR, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_XorExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_XorExpression" ): + listener.enterOC_XorExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_XorExpression" ): + listener.exitOC_XorExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_XorExpression" ): + return visitor.visitOC_XorExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_XorExpression(self): + + localctx = CypherParser.OC_XorExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 106, self.RULE_oC_XorExpression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 913 + self.oC_AndExpression() + self.state = 920 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,152,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 914 + self.match(CypherParser.SP) + self.state = 915 + self.match(CypherParser.XOR) + self.state = 916 + self.match(CypherParser.SP) + self.state = 917 + self.oC_AndExpression() + self.state = 922 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,152,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_AndExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_NotExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_NotExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_NotExpressionContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def AND(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.AND) + else: + return self.getToken(CypherParser.AND, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_AndExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_AndExpression" ): + listener.enterOC_AndExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_AndExpression" ): + listener.exitOC_AndExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_AndExpression" ): + return visitor.visitOC_AndExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_AndExpression(self): + + localctx = CypherParser.OC_AndExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 108, self.RULE_oC_AndExpression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 923 + self.oC_NotExpression() + self.state = 930 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,153,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 924 + self.match(CypherParser.SP) + self.state = 925 + self.match(CypherParser.AND) + self.state = 926 + self.match(CypherParser.SP) + self.state = 927 + self.oC_NotExpression() + self.state = 932 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,153,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_NotExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_ComparisonExpression(self): + return self.getTypedRuleContext(CypherParser.OC_ComparisonExpressionContext,0) + + + def NOT(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.NOT) + else: + return self.getToken(CypherParser.NOT, i) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_NotExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_NotExpression" ): + listener.enterOC_NotExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_NotExpression" ): + listener.exitOC_NotExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_NotExpression" ): + return visitor.visitOC_NotExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_NotExpression(self): + + localctx = CypherParser.OC_NotExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 110, self.RULE_oC_NotExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 939 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==76: + self.state = 933 + self.match(CypherParser.NOT) + self.state = 935 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 934 + self.match(CypherParser.SP) + + + self.state = 941 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 942 + self.oC_ComparisonExpression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ComparisonExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_StringListNullPredicateExpression(self): + return self.getTypedRuleContext(CypherParser.OC_StringListNullPredicateExpressionContext,0) + + + def oC_PartialComparisonExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_PartialComparisonExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_PartialComparisonExpressionContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_ComparisonExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ComparisonExpression" ): + listener.enterOC_ComparisonExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ComparisonExpression" ): + listener.exitOC_ComparisonExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ComparisonExpression" ): + return visitor.visitOC_ComparisonExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ComparisonExpression(self): + + localctx = CypherParser.OC_ComparisonExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 112, self.RULE_oC_ComparisonExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 944 + self.oC_StringListNullPredicateExpression() + self.state = 951 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,157,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 946 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 945 + self.match(CypherParser.SP) + + + self.state = 948 + self.oC_PartialComparisonExpression() + self.state = 953 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,157,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PartialComparisonExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_StringListNullPredicateExpression(self): + return self.getTypedRuleContext(CypherParser.OC_StringListNullPredicateExpressionContext,0) + + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_PartialComparisonExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_PartialComparisonExpression" ): + listener.enterOC_PartialComparisonExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_PartialComparisonExpression" ): + listener.exitOC_PartialComparisonExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_PartialComparisonExpression" ): + return visitor.visitOC_PartialComparisonExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_PartialComparisonExpression(self): + + localctx = CypherParser.OC_PartialComparisonExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 114, self.RULE_oC_PartialComparisonExpression) + self._la = 0 # Token type + try: + self.state = 984 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [3]: + self.enterOuterAlt(localctx, 1) + self.state = 954 + self.match(CypherParser.T__2) + self.state = 956 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 955 + self.match(CypherParser.SP) + + + self.state = 958 + self.oC_StringListNullPredicateExpression() + pass + elif token in [13]: + self.enterOuterAlt(localctx, 2) + self.state = 959 + self.match(CypherParser.T__12) + self.state = 961 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 960 + self.match(CypherParser.SP) + + + self.state = 963 + self.oC_StringListNullPredicateExpression() + pass + elif token in [14]: + self.enterOuterAlt(localctx, 3) + self.state = 964 + self.match(CypherParser.T__13) + self.state = 966 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 965 + self.match(CypherParser.SP) + + + self.state = 968 + self.oC_StringListNullPredicateExpression() + pass + elif token in [15]: + self.enterOuterAlt(localctx, 4) + self.state = 969 + self.match(CypherParser.T__14) + self.state = 971 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 970 + self.match(CypherParser.SP) + + + self.state = 973 + self.oC_StringListNullPredicateExpression() + pass + elif token in [16]: + self.enterOuterAlt(localctx, 5) + self.state = 974 + self.match(CypherParser.T__15) + self.state = 976 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 975 + self.match(CypherParser.SP) + + + self.state = 978 + self.oC_StringListNullPredicateExpression() + pass + elif token in [17]: + self.enterOuterAlt(localctx, 6) + self.state = 979 + self.match(CypherParser.T__16) + self.state = 981 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 980 + self.match(CypherParser.SP) + + + self.state = 983 + self.oC_StringListNullPredicateExpression() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_StringListNullPredicateExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_AddOrSubtractExpression(self): + return self.getTypedRuleContext(CypherParser.OC_AddOrSubtractExpressionContext,0) + + + def oC_StringPredicateExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_StringPredicateExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_StringPredicateExpressionContext,i) + + + def oC_ListPredicateExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ListPredicateExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ListPredicateExpressionContext,i) + + + def oC_NullPredicateExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_NullPredicateExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_NullPredicateExpressionContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_StringListNullPredicateExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_StringListNullPredicateExpression" ): + listener.enterOC_StringListNullPredicateExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_StringListNullPredicateExpression" ): + listener.exitOC_StringListNullPredicateExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_StringListNullPredicateExpression" ): + return visitor.visitOC_StringListNullPredicateExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_StringListNullPredicateExpression(self): + + localctx = CypherParser.OC_StringListNullPredicateExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 116, self.RULE_oC_StringListNullPredicateExpression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 986 + self.oC_AddOrSubtractExpression() + self.state = 992 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,166,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 990 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,165,self._ctx) + if la_ == 1: + self.state = 987 + self.oC_StringPredicateExpression() + pass + + elif la_ == 2: + self.state = 988 + self.oC_ListPredicateExpression() + pass + + elif la_ == 3: + self.state = 989 + self.oC_NullPredicateExpression() + pass + + + self.state = 994 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,166,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_StringPredicateExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_AddOrSubtractExpression(self): + return self.getTypedRuleContext(CypherParser.OC_AddOrSubtractExpressionContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def STARTS(self): + return self.getToken(CypherParser.STARTS, 0) + + def WITH(self): + return self.getToken(CypherParser.WITH, 0) + + def ENDS(self): + return self.getToken(CypherParser.ENDS, 0) + + def CONTAINS(self): + return self.getToken(CypherParser.CONTAINS, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_StringPredicateExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_StringPredicateExpression" ): + listener.enterOC_StringPredicateExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_StringPredicateExpression" ): + listener.exitOC_StringPredicateExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_StringPredicateExpression" ): + return visitor.visitOC_StringPredicateExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_StringPredicateExpression(self): + + localctx = CypherParser.OC_StringPredicateExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 118, self.RULE_oC_StringPredicateExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1005 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,167,self._ctx) + if la_ == 1: + self.state = 995 + self.match(CypherParser.SP) + self.state = 996 + self.match(CypherParser.STARTS) + self.state = 997 + self.match(CypherParser.SP) + self.state = 998 + self.match(CypherParser.WITH) + pass + + elif la_ == 2: + self.state = 999 + self.match(CypherParser.SP) + self.state = 1000 + self.match(CypherParser.ENDS) + self.state = 1001 + self.match(CypherParser.SP) + self.state = 1002 + self.match(CypherParser.WITH) + pass + + elif la_ == 3: + self.state = 1003 + self.match(CypherParser.SP) + self.state = 1004 + self.match(CypherParser.CONTAINS) + pass + + + self.state = 1008 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1007 + self.match(CypherParser.SP) + + + self.state = 1010 + self.oC_AddOrSubtractExpression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ListPredicateExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def IN(self): + return self.getToken(CypherParser.IN, 0) + + def oC_AddOrSubtractExpression(self): + return self.getTypedRuleContext(CypherParser.OC_AddOrSubtractExpressionContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ListPredicateExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ListPredicateExpression" ): + listener.enterOC_ListPredicateExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ListPredicateExpression" ): + listener.exitOC_ListPredicateExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ListPredicateExpression" ): + return visitor.visitOC_ListPredicateExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ListPredicateExpression(self): + + localctx = CypherParser.OC_ListPredicateExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 120, self.RULE_oC_ListPredicateExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1012 + self.match(CypherParser.SP) + self.state = 1013 + self.match(CypherParser.IN) + self.state = 1015 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1014 + self.match(CypherParser.SP) + + + self.state = 1017 + self.oC_AddOrSubtractExpression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_NullPredicateExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def IS(self): + return self.getToken(CypherParser.IS, 0) + + def NULL(self): + return self.getToken(CypherParser.NULL, 0) + + def NOT(self): + return self.getToken(CypherParser.NOT, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_NullPredicateExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_NullPredicateExpression" ): + listener.enterOC_NullPredicateExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_NullPredicateExpression" ): + listener.exitOC_NullPredicateExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_NullPredicateExpression" ): + return visitor.visitOC_NullPredicateExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_NullPredicateExpression(self): + + localctx = CypherParser.OC_NullPredicateExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 122, self.RULE_oC_NullPredicateExpression) + try: + self.state = 1029 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,170,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 1019 + self.match(CypherParser.SP) + self.state = 1020 + self.match(CypherParser.IS) + self.state = 1021 + self.match(CypherParser.SP) + self.state = 1022 + self.match(CypherParser.NULL) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 1023 + self.match(CypherParser.SP) + self.state = 1024 + self.match(CypherParser.IS) + self.state = 1025 + self.match(CypherParser.SP) + self.state = 1026 + self.match(CypherParser.NOT) + self.state = 1027 + self.match(CypherParser.SP) + self.state = 1028 + self.match(CypherParser.NULL) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_AddOrSubtractExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_MultiplyDivideModuloExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_MultiplyDivideModuloExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_MultiplyDivideModuloExpressionContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_AddOrSubtractExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_AddOrSubtractExpression" ): + listener.enterOC_AddOrSubtractExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_AddOrSubtractExpression" ): + listener.exitOC_AddOrSubtractExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_AddOrSubtractExpression" ): + return visitor.visitOC_AddOrSubtractExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_AddOrSubtractExpression(self): + + localctx = CypherParser.OC_AddOrSubtractExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 124, self.RULE_oC_AddOrSubtractExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1031 + self.oC_MultiplyDivideModuloExpression() + self.state = 1050 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,176,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1048 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,175,self._ctx) + if la_ == 1: + self.state = 1033 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1032 + self.match(CypherParser.SP) + + + self.state = 1035 + self.match(CypherParser.T__17) + self.state = 1037 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1036 + self.match(CypherParser.SP) + + + self.state = 1039 + self.oC_MultiplyDivideModuloExpression() + pass + + elif la_ == 2: + self.state = 1041 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1040 + self.match(CypherParser.SP) + + + self.state = 1043 + self.match(CypherParser.T__18) + self.state = 1045 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1044 + self.match(CypherParser.SP) + + + self.state = 1047 + self.oC_MultiplyDivideModuloExpression() + pass + + + self.state = 1052 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,176,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_MultiplyDivideModuloExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_PowerOfExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_PowerOfExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_PowerOfExpressionContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_MultiplyDivideModuloExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_MultiplyDivideModuloExpression" ): + listener.enterOC_MultiplyDivideModuloExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_MultiplyDivideModuloExpression" ): + listener.exitOC_MultiplyDivideModuloExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_MultiplyDivideModuloExpression" ): + return visitor.visitOC_MultiplyDivideModuloExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_MultiplyDivideModuloExpression(self): + + localctx = CypherParser.OC_MultiplyDivideModuloExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 126, self.RULE_oC_MultiplyDivideModuloExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1053 + self.oC_PowerOfExpression() + self.state = 1080 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,184,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1078 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,183,self._ctx) + if la_ == 1: + self.state = 1055 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1054 + self.match(CypherParser.SP) + + + self.state = 1057 + self.match(CypherParser.T__4) + self.state = 1059 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1058 + self.match(CypherParser.SP) + + + self.state = 1061 + self.oC_PowerOfExpression() + pass + + elif la_ == 2: + self.state = 1063 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1062 + self.match(CypherParser.SP) + + + self.state = 1065 + self.match(CypherParser.T__19) + self.state = 1067 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1066 + self.match(CypherParser.SP) + + + self.state = 1069 + self.oC_PowerOfExpression() + pass + + elif la_ == 3: + self.state = 1071 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1070 + self.match(CypherParser.SP) + + + self.state = 1073 + self.match(CypherParser.T__20) + self.state = 1075 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1074 + self.match(CypherParser.SP) + + + self.state = 1077 + self.oC_PowerOfExpression() + pass + + + self.state = 1082 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,184,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PowerOfExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_UnaryAddOrSubtractExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_UnaryAddOrSubtractExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_UnaryAddOrSubtractExpressionContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_PowerOfExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_PowerOfExpression" ): + listener.enterOC_PowerOfExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_PowerOfExpression" ): + listener.exitOC_PowerOfExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_PowerOfExpression" ): + return visitor.visitOC_PowerOfExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_PowerOfExpression(self): + + localctx = CypherParser.OC_PowerOfExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 128, self.RULE_oC_PowerOfExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1083 + self.oC_UnaryAddOrSubtractExpression() + self.state = 1094 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,187,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1085 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1084 + self.match(CypherParser.SP) + + + self.state = 1087 + self.match(CypherParser.T__21) + self.state = 1089 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1088 + self.match(CypherParser.SP) + + + self.state = 1091 + self.oC_UnaryAddOrSubtractExpression() + self.state = 1096 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,187,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_UnaryAddOrSubtractExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_NonArithmeticOperatorExpression(self): + return self.getTypedRuleContext(CypherParser.OC_NonArithmeticOperatorExpressionContext,0) + + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_UnaryAddOrSubtractExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_UnaryAddOrSubtractExpression" ): + listener.enterOC_UnaryAddOrSubtractExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_UnaryAddOrSubtractExpression" ): + listener.exitOC_UnaryAddOrSubtractExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_UnaryAddOrSubtractExpression" ): + return visitor.visitOC_UnaryAddOrSubtractExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_UnaryAddOrSubtractExpression(self): + + localctx = CypherParser.OC_UnaryAddOrSubtractExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 130, self.RULE_oC_UnaryAddOrSubtractExpression) + self._la = 0 # Token type + try: + self.state = 1103 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [6, 8, 24, 26, 47, 82, 83, 84, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 105, 106, 107, 119, 120, 121, 124]: + self.enterOuterAlt(localctx, 1) + self.state = 1097 + self.oC_NonArithmeticOperatorExpression() + pass + elif token in [18, 19]: + self.enterOuterAlt(localctx, 2) + self.state = 1098 + _la = self._input.LA(1) + if not(_la==18 or _la==19): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 1100 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1099 + self.match(CypherParser.SP) + + + self.state = 1102 + self.oC_NonArithmeticOperatorExpression() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_NonArithmeticOperatorExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Atom(self): + return self.getTypedRuleContext(CypherParser.OC_AtomContext,0) + + + def oC_NodeLabels(self): + return self.getTypedRuleContext(CypherParser.OC_NodeLabelsContext,0) + + + def oC_ListOperatorExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ListOperatorExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ListOperatorExpressionContext,i) + + + def oC_PropertyLookup(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_PropertyLookupContext) + else: + return self.getTypedRuleContext(CypherParser.OC_PropertyLookupContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_NonArithmeticOperatorExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_NonArithmeticOperatorExpression" ): + listener.enterOC_NonArithmeticOperatorExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_NonArithmeticOperatorExpression" ): + listener.exitOC_NonArithmeticOperatorExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_NonArithmeticOperatorExpression" ): + return visitor.visitOC_NonArithmeticOperatorExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_NonArithmeticOperatorExpression(self): + + localctx = CypherParser.OC_NonArithmeticOperatorExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 132, self.RULE_oC_NonArithmeticOperatorExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1105 + self.oC_Atom() + self.state = 1116 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,193,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1114 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,192,self._ctx) + if la_ == 1: + self.state = 1107 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1106 + self.match(CypherParser.SP) + + + self.state = 1109 + self.oC_ListOperatorExpression() + pass + + elif la_ == 2: + self.state = 1111 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1110 + self.match(CypherParser.SP) + + + self.state = 1113 + self.oC_PropertyLookup() + pass + + + self.state = 1118 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,193,self._ctx) + + self.state = 1123 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,195,self._ctx) + if la_ == 1: + self.state = 1120 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1119 + self.match(CypherParser.SP) + + + self.state = 1122 + self.oC_NodeLabels() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ListOperatorExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ListOperatorExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ListOperatorExpression" ): + listener.enterOC_ListOperatorExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ListOperatorExpression" ): + listener.exitOC_ListOperatorExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ListOperatorExpression" ): + return visitor.visitOC_ListOperatorExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ListOperatorExpression(self): + + localctx = CypherParser.OC_ListOperatorExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 134, self.RULE_oC_ListOperatorExpression) + self._la = 0 # Token type + try: + self.state = 1138 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,198,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 1125 + self.match(CypherParser.T__7) + self.state = 1126 + self.oC_Expression() + self.state = 1127 + self.match(CypherParser.T__8) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 1129 + self.match(CypherParser.T__7) + self.state = 1131 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & 140737573028160) != 0) or ((((_la - 76)) & ~0x3f) == 0 and ((1 << (_la - 76)) & 343051394343361) != 0): + self.state = 1130 + self.oC_Expression() + + + self.state = 1133 + self.match(CypherParser.T__11) + self.state = 1135 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & 140737573028160) != 0) or ((((_la - 76)) & ~0x3f) == 0 and ((1 << (_la - 76)) & 343051394343361) != 0): + self.state = 1134 + self.oC_Expression() + + + self.state = 1137 + self.match(CypherParser.T__8) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PropertyLookupContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_PropertyKeyName(self): + return self.getTypedRuleContext(CypherParser.OC_PropertyKeyNameContext,0) + + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_PropertyLookup + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_PropertyLookup" ): + listener.enterOC_PropertyLookup(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_PropertyLookup" ): + listener.exitOC_PropertyLookup(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_PropertyLookup" ): + return visitor.visitOC_PropertyLookup(self) + else: + return visitor.visitChildren(self) + + + + + def oC_PropertyLookup(self): + + localctx = CypherParser.OC_PropertyLookupContext(self, self._ctx, self.state) + self.enterRule(localctx, 136, self.RULE_oC_PropertyLookup) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1140 + self.match(CypherParser.T__22) + self.state = 1142 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1141 + self.match(CypherParser.SP) + + + self.state = 1144 + self.oC_PropertyKeyName() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_AtomContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Literal(self): + return self.getTypedRuleContext(CypherParser.OC_LiteralContext,0) + + + def oC_Parameter(self): + return self.getTypedRuleContext(CypherParser.OC_ParameterContext,0) + + + def oC_CaseExpression(self): + return self.getTypedRuleContext(CypherParser.OC_CaseExpressionContext,0) + + + def COUNT(self): + return self.getToken(CypherParser.COUNT, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_ListComprehension(self): + return self.getTypedRuleContext(CypherParser.OC_ListComprehensionContext,0) + + + def oC_PatternComprehension(self): + return self.getTypedRuleContext(CypherParser.OC_PatternComprehensionContext,0) + + + def oC_Quantifier(self): + return self.getTypedRuleContext(CypherParser.OC_QuantifierContext,0) + + + def oC_PatternPredicate(self): + return self.getTypedRuleContext(CypherParser.OC_PatternPredicateContext,0) + + + def oC_ParenthesizedExpression(self): + return self.getTypedRuleContext(CypherParser.OC_ParenthesizedExpressionContext,0) + + + def oC_FunctionInvocation(self): + return self.getTypedRuleContext(CypherParser.OC_FunctionInvocationContext,0) + + + def oC_ExistentialSubquery(self): + return self.getTypedRuleContext(CypherParser.OC_ExistentialSubqueryContext,0) + + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Atom + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Atom" ): + listener.enterOC_Atom(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Atom" ): + listener.exitOC_Atom(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Atom" ): + return visitor.visitOC_Atom(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Atom(self): + + localctx = CypherParser.OC_AtomContext(self, self._ctx, self.state) + self.enterRule(localctx, 138, self.RULE_oC_Atom) + self._la = 0 # Token type + try: + self.state = 1170 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,203,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 1146 + self.oC_Literal() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 1147 + self.oC_Parameter() + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 1148 + self.oC_CaseExpression() + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 1149 + self.match(CypherParser.COUNT) + self.state = 1151 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1150 + self.match(CypherParser.SP) + + + self.state = 1153 + self.match(CypherParser.T__5) + self.state = 1155 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1154 + self.match(CypherParser.SP) + + + self.state = 1157 + self.match(CypherParser.T__4) + self.state = 1159 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1158 + self.match(CypherParser.SP) + + + self.state = 1161 + self.match(CypherParser.T__6) + pass + + elif la_ == 5: + self.enterOuterAlt(localctx, 5) + self.state = 1162 + self.oC_ListComprehension() + pass + + elif la_ == 6: + self.enterOuterAlt(localctx, 6) + self.state = 1163 + self.oC_PatternComprehension() + pass + + elif la_ == 7: + self.enterOuterAlt(localctx, 7) + self.state = 1164 + self.oC_Quantifier() + pass + + elif la_ == 8: + self.enterOuterAlt(localctx, 8) + self.state = 1165 + self.oC_PatternPredicate() + pass + + elif la_ == 9: + self.enterOuterAlt(localctx, 9) + self.state = 1166 + self.oC_ParenthesizedExpression() + pass + + elif la_ == 10: + self.enterOuterAlt(localctx, 10) + self.state = 1167 + self.oC_FunctionInvocation() + pass + + elif la_ == 11: + self.enterOuterAlt(localctx, 11) + self.state = 1168 + self.oC_ExistentialSubquery() + pass + + elif la_ == 12: + self.enterOuterAlt(localctx, 12) + self.state = 1169 + self.oC_Variable() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_CaseExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def END(self): + return self.getToken(CypherParser.END, 0) + + def ELSE(self): + return self.getToken(CypherParser.ELSE, 0) + + def oC_Expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,i) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def CASE(self): + return self.getToken(CypherParser.CASE, 0) + + def oC_CaseAlternative(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_CaseAlternativeContext) + else: + return self.getTypedRuleContext(CypherParser.OC_CaseAlternativeContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_CaseExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_CaseExpression" ): + listener.enterOC_CaseExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_CaseExpression" ): + listener.exitOC_CaseExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_CaseExpression" ): + return visitor.visitOC_CaseExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_CaseExpression(self): + + localctx = CypherParser.OC_CaseExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 140, self.RULE_oC_CaseExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1194 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,209,self._ctx) + if la_ == 1: + self.state = 1172 + self.match(CypherParser.CASE) + self.state = 1177 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 1174 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1173 + self.match(CypherParser.SP) + + + self.state = 1176 + self.oC_CaseAlternative() + + else: + raise NoViableAltException(self) + self.state = 1179 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,205,self._ctx) + + pass + + elif la_ == 2: + self.state = 1181 + self.match(CypherParser.CASE) + self.state = 1183 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1182 + self.match(CypherParser.SP) + + + self.state = 1185 + self.oC_Expression() + self.state = 1190 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 1187 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1186 + self.match(CypherParser.SP) + + + self.state = 1189 + self.oC_CaseAlternative() + + else: + raise NoViableAltException(self) + self.state = 1192 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,208,self._ctx) + + pass + + + self.state = 1204 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,212,self._ctx) + if la_ == 1: + self.state = 1197 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1196 + self.match(CypherParser.SP) + + + self.state = 1199 + self.match(CypherParser.ELSE) + self.state = 1201 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1200 + self.match(CypherParser.SP) + + + self.state = 1203 + self.oC_Expression() + + + self.state = 1207 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1206 + self.match(CypherParser.SP) + + + self.state = 1209 + self.match(CypherParser.END) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_CaseAlternativeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def WHEN(self): + return self.getToken(CypherParser.WHEN, 0) + + def oC_Expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,i) + + + def THEN(self): + return self.getToken(CypherParser.THEN, 0) + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_CaseAlternative + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_CaseAlternative" ): + listener.enterOC_CaseAlternative(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_CaseAlternative" ): + listener.exitOC_CaseAlternative(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_CaseAlternative" ): + return visitor.visitOC_CaseAlternative(self) + else: + return visitor.visitChildren(self) + + + + + def oC_CaseAlternative(self): + + localctx = CypherParser.OC_CaseAlternativeContext(self, self._ctx, self.state) + self.enterRule(localctx, 142, self.RULE_oC_CaseAlternative) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1211 + self.match(CypherParser.WHEN) + self.state = 1213 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1212 + self.match(CypherParser.SP) + + + self.state = 1215 + self.oC_Expression() + self.state = 1217 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1216 + self.match(CypherParser.SP) + + + self.state = 1219 + self.match(CypherParser.THEN) + self.state = 1221 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1220 + self.match(CypherParser.SP) + + + self.state = 1223 + self.oC_Expression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ListComprehensionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_FilterExpression(self): + return self.getTypedRuleContext(CypherParser.OC_FilterExpressionContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ListComprehension + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ListComprehension" ): + listener.enterOC_ListComprehension(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ListComprehension" ): + listener.exitOC_ListComprehension(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ListComprehension" ): + return visitor.visitOC_ListComprehension(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ListComprehension(self): + + localctx = CypherParser.OC_ListComprehensionContext(self, self._ctx, self.state) + self.enterRule(localctx, 144, self.RULE_oC_ListComprehension) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1225 + self.match(CypherParser.T__7) + self.state = 1227 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1226 + self.match(CypherParser.SP) + + + self.state = 1229 + self.oC_FilterExpression() + self.state = 1238 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,220,self._ctx) + if la_ == 1: + self.state = 1231 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1230 + self.match(CypherParser.SP) + + + self.state = 1233 + self.match(CypherParser.T__10) + self.state = 1235 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1234 + self.match(CypherParser.SP) + + + self.state = 1237 + self.oC_Expression() + + + self.state = 1241 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1240 + self.match(CypherParser.SP) + + + self.state = 1243 + self.match(CypherParser.T__8) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PatternComprehensionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_RelationshipsPattern(self): + return self.getTypedRuleContext(CypherParser.OC_RelationshipsPatternContext,0) + + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def oC_Where(self): + return self.getTypedRuleContext(CypherParser.OC_WhereContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_PatternComprehension + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_PatternComprehension" ): + listener.enterOC_PatternComprehension(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_PatternComprehension" ): + listener.exitOC_PatternComprehension(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_PatternComprehension" ): + return visitor.visitOC_PatternComprehension(self) + else: + return visitor.visitChildren(self) + + + + + def oC_PatternComprehension(self): + + localctx = CypherParser.OC_PatternComprehensionContext(self, self._ctx, self.state) + self.enterRule(localctx, 146, self.RULE_oC_PatternComprehension) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1245 + self.match(CypherParser.T__7) + self.state = 1247 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1246 + self.match(CypherParser.SP) + + + self.state = 1257 + self._errHandler.sync(self) + _la = self._input.LA(1) + if ((((_la - 83)) & ~0x3f) == 0 and ((1 << (_la - 83)) & 2680059625921) != 0): + self.state = 1249 + self.oC_Variable() + self.state = 1251 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1250 + self.match(CypherParser.SP) + + + self.state = 1253 + self.match(CypherParser.T__2) + self.state = 1255 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1254 + self.match(CypherParser.SP) + + + + + self.state = 1259 + self.oC_RelationshipsPattern() + self.state = 1261 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1260 + self.match(CypherParser.SP) + + + self.state = 1267 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==72: + self.state = 1263 + self.oC_Where() + self.state = 1265 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1264 + self.match(CypherParser.SP) + + + + + self.state = 1269 + self.match(CypherParser.T__10) + self.state = 1271 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1270 + self.match(CypherParser.SP) + + + self.state = 1273 + self.oC_Expression() + self.state = 1275 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1274 + self.match(CypherParser.SP) + + + self.state = 1277 + self.match(CypherParser.T__8) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_QuantifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ALL(self): + return self.getToken(CypherParser.ALL, 0) + + def oC_FilterExpression(self): + return self.getTypedRuleContext(CypherParser.OC_FilterExpressionContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def ANY(self): + return self.getToken(CypherParser.ANY, 0) + + def NONE(self): + return self.getToken(CypherParser.NONE, 0) + + def SINGLE(self): + return self.getToken(CypherParser.SINGLE, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_Quantifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Quantifier" ): + listener.enterOC_Quantifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Quantifier" ): + listener.exitOC_Quantifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Quantifier" ): + return visitor.visitOC_Quantifier(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Quantifier(self): + + localctx = CypherParser.OC_QuantifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 148, self.RULE_oC_Quantifier) + self._la = 0 # Token type + try: + self.state = 1335 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [47]: + self.enterOuterAlt(localctx, 1) + self.state = 1279 + self.match(CypherParser.ALL) + self.state = 1281 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1280 + self.match(CypherParser.SP) + + + self.state = 1283 + self.match(CypherParser.T__5) + self.state = 1285 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1284 + self.match(CypherParser.SP) + + + self.state = 1287 + self.oC_FilterExpression() + self.state = 1289 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1288 + self.match(CypherParser.SP) + + + self.state = 1291 + self.match(CypherParser.T__6) + pass + elif token in [89]: + self.enterOuterAlt(localctx, 2) + self.state = 1293 + self.match(CypherParser.ANY) + self.state = 1295 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1294 + self.match(CypherParser.SP) + + + self.state = 1297 + self.match(CypherParser.T__5) + self.state = 1299 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1298 + self.match(CypherParser.SP) + + + self.state = 1301 + self.oC_FilterExpression() + self.state = 1303 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1302 + self.match(CypherParser.SP) + + + self.state = 1305 + self.match(CypherParser.T__6) + pass + elif token in [90]: + self.enterOuterAlt(localctx, 3) + self.state = 1307 + self.match(CypherParser.NONE) + self.state = 1309 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1308 + self.match(CypherParser.SP) + + + self.state = 1311 + self.match(CypherParser.T__5) + self.state = 1313 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1312 + self.match(CypherParser.SP) + + + self.state = 1315 + self.oC_FilterExpression() + self.state = 1317 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1316 + self.match(CypherParser.SP) + + + self.state = 1319 + self.match(CypherParser.T__6) + pass + elif token in [91]: + self.enterOuterAlt(localctx, 4) + self.state = 1321 + self.match(CypherParser.SINGLE) + self.state = 1323 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1322 + self.match(CypherParser.SP) + + + self.state = 1325 + self.match(CypherParser.T__5) + self.state = 1327 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1326 + self.match(CypherParser.SP) + + + self.state = 1329 + self.oC_FilterExpression() + self.state = 1331 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1330 + self.match(CypherParser.SP) + + + self.state = 1333 + self.match(CypherParser.T__6) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_FilterExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_IdInColl(self): + return self.getTypedRuleContext(CypherParser.OC_IdInCollContext,0) + + + def oC_Where(self): + return self.getTypedRuleContext(CypherParser.OC_WhereContext,0) + + + def SP(self): + return self.getToken(CypherParser.SP, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_FilterExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_FilterExpression" ): + listener.enterOC_FilterExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_FilterExpression" ): + listener.exitOC_FilterExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_FilterExpression" ): + return visitor.visitOC_FilterExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_FilterExpression(self): + + localctx = CypherParser.OC_FilterExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 150, self.RULE_oC_FilterExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1337 + self.oC_IdInColl() + self.state = 1342 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,245,self._ctx) + if la_ == 1: + self.state = 1339 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1338 + self.match(CypherParser.SP) + + + self.state = 1341 + self.oC_Where() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PatternPredicateContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_RelationshipsPattern(self): + return self.getTypedRuleContext(CypherParser.OC_RelationshipsPatternContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_PatternPredicate + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_PatternPredicate" ): + listener.enterOC_PatternPredicate(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_PatternPredicate" ): + listener.exitOC_PatternPredicate(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_PatternPredicate" ): + return visitor.visitOC_PatternPredicate(self) + else: + return visitor.visitChildren(self) + + + + + def oC_PatternPredicate(self): + + localctx = CypherParser.OC_PatternPredicateContext(self, self._ctx, self.state) + self.enterRule(localctx, 152, self.RULE_oC_PatternPredicate) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1344 + self.oC_RelationshipsPattern() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ParenthesizedExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def getRuleIndex(self): + return CypherParser.RULE_oC_ParenthesizedExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ParenthesizedExpression" ): + listener.enterOC_ParenthesizedExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ParenthesizedExpression" ): + listener.exitOC_ParenthesizedExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ParenthesizedExpression" ): + return visitor.visitOC_ParenthesizedExpression(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ParenthesizedExpression(self): + + localctx = CypherParser.OC_ParenthesizedExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 154, self.RULE_oC_ParenthesizedExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1346 + self.match(CypherParser.T__5) + self.state = 1348 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1347 + self.match(CypherParser.SP) + + + self.state = 1350 + self.oC_Expression() + self.state = 1352 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1351 + self.match(CypherParser.SP) + + + self.state = 1354 + self.match(CypherParser.T__6) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_IdInCollContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Variable(self): + return self.getTypedRuleContext(CypherParser.OC_VariableContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def IN(self): + return self.getToken(CypherParser.IN, 0) + + def oC_Expression(self): + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_IdInColl + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_IdInColl" ): + listener.enterOC_IdInColl(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_IdInColl" ): + listener.exitOC_IdInColl(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_IdInColl" ): + return visitor.visitOC_IdInColl(self) + else: + return visitor.visitChildren(self) + + + + + def oC_IdInColl(self): + + localctx = CypherParser.OC_IdInCollContext(self, self._ctx, self.state) + self.enterRule(localctx, 156, self.RULE_oC_IdInColl) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1356 + self.oC_Variable() + self.state = 1357 + self.match(CypherParser.SP) + self.state = 1358 + self.match(CypherParser.IN) + self.state = 1359 + self.match(CypherParser.SP) + self.state = 1360 + self.oC_Expression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_FunctionInvocationContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_FunctionName(self): + return self.getTypedRuleContext(CypherParser.OC_FunctionNameContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def DISTINCT(self): + return self.getToken(CypherParser.DISTINCT, 0) + + def oC_Expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_FunctionInvocation + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_FunctionInvocation" ): + listener.enterOC_FunctionInvocation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_FunctionInvocation" ): + listener.exitOC_FunctionInvocation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_FunctionInvocation" ): + return visitor.visitOC_FunctionInvocation(self) + else: + return visitor.visitChildren(self) + + + + + def oC_FunctionInvocation(self): + + localctx = CypherParser.OC_FunctionInvocationContext(self, self._ctx, self.state) + self.enterRule(localctx, 158, self.RULE_oC_FunctionInvocation) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1362 + self.oC_FunctionName() + self.state = 1364 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1363 + self.match(CypherParser.SP) + + + self.state = 1366 + self.match(CypherParser.T__5) + self.state = 1368 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1367 + self.match(CypherParser.SP) + + + self.state = 1374 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==63: + self.state = 1370 + self.match(CypherParser.DISTINCT) + self.state = 1372 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1371 + self.match(CypherParser.SP) + + + + + self.state = 1393 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & 140737573028160) != 0) or ((((_la - 76)) & ~0x3f) == 0 and ((1 << (_la - 76)) & 343051394343361) != 0): + self.state = 1376 + self.oC_Expression() + self.state = 1378 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1377 + self.match(CypherParser.SP) + + + self.state = 1390 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==2: + self.state = 1380 + self.match(CypherParser.T__1) + self.state = 1382 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1381 + self.match(CypherParser.SP) + + + self.state = 1384 + self.oC_Expression() + self.state = 1386 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1385 + self.match(CypherParser.SP) + + + self.state = 1392 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 1395 + self.match(CypherParser.T__6) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_FunctionNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Namespace(self): + return self.getTypedRuleContext(CypherParser.OC_NamespaceContext,0) + + + def oC_SymbolicName(self): + return self.getTypedRuleContext(CypherParser.OC_SymbolicNameContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_FunctionName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_FunctionName" ): + listener.enterOC_FunctionName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_FunctionName" ): + listener.exitOC_FunctionName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_FunctionName" ): + return visitor.visitOC_FunctionName(self) + else: + return visitor.visitChildren(self) + + + + + def oC_FunctionName(self): + + localctx = CypherParser.OC_FunctionNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 160, self.RULE_oC_FunctionName) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1397 + self.oC_Namespace() + self.state = 1398 + self.oC_SymbolicName() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ExistentialSubqueryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def EXISTS(self): + return self.getToken(CypherParser.EXISTS, 0) + + def oC_RegularQuery(self): + return self.getTypedRuleContext(CypherParser.OC_RegularQueryContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_Pattern(self): + return self.getTypedRuleContext(CypherParser.OC_PatternContext,0) + + + def oC_Where(self): + return self.getTypedRuleContext(CypherParser.OC_WhereContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ExistentialSubquery + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ExistentialSubquery" ): + listener.enterOC_ExistentialSubquery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ExistentialSubquery" ): + listener.exitOC_ExistentialSubquery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ExistentialSubquery" ): + return visitor.visitOC_ExistentialSubquery(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ExistentialSubquery(self): + + localctx = CypherParser.OC_ExistentialSubqueryContext(self, self._ctx, self.state) + self.enterRule(localctx, 162, self.RULE_oC_ExistentialSubquery) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1400 + self.match(CypherParser.EXISTS) + self.state = 1402 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1401 + self.match(CypherParser.SP) + + + self.state = 1404 + self.match(CypherParser.T__23) + self.state = 1406 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1405 + self.match(CypherParser.SP) + + + self.state = 1416 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [48, 49, 50, 52, 54, 55, 56, 57, 58, 59, 61, 62]: + self.state = 1408 + self.oC_RegularQuery() + pass + elif token in [6, 83, 89, 90, 91, 98, 119, 120, 121, 124]: + self.state = 1409 + self.oC_Pattern() + self.state = 1414 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,260,self._ctx) + if la_ == 1: + self.state = 1411 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1410 + self.match(CypherParser.SP) + + + self.state = 1413 + self.oC_Where() + + + pass + else: + raise NoViableAltException(self) + + self.state = 1419 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1418 + self.match(CypherParser.SP) + + + self.state = 1421 + self.match(CypherParser.T__24) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ExplicitProcedureInvocationContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_ProcedureName(self): + return self.getTypedRuleContext(CypherParser.OC_ProcedureNameContext,0) + + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_Expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ExplicitProcedureInvocation + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ExplicitProcedureInvocation" ): + listener.enterOC_ExplicitProcedureInvocation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ExplicitProcedureInvocation" ): + listener.exitOC_ExplicitProcedureInvocation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ExplicitProcedureInvocation" ): + return visitor.visitOC_ExplicitProcedureInvocation(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ExplicitProcedureInvocation(self): + + localctx = CypherParser.OC_ExplicitProcedureInvocationContext(self, self._ctx, self.state) + self.enterRule(localctx, 164, self.RULE_oC_ExplicitProcedureInvocation) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1423 + self.oC_ProcedureName() + self.state = 1425 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1424 + self.match(CypherParser.SP) + + + self.state = 1427 + self.match(CypherParser.T__5) + self.state = 1429 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1428 + self.match(CypherParser.SP) + + + self.state = 1448 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & 140737573028160) != 0) or ((((_la - 76)) & ~0x3f) == 0 and ((1 << (_la - 76)) & 343051394343361) != 0): + self.state = 1431 + self.oC_Expression() + self.state = 1433 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1432 + self.match(CypherParser.SP) + + + self.state = 1445 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==2: + self.state = 1435 + self.match(CypherParser.T__1) + self.state = 1437 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1436 + self.match(CypherParser.SP) + + + self.state = 1439 + self.oC_Expression() + self.state = 1441 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1440 + self.match(CypherParser.SP) + + + self.state = 1447 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 1450 + self.match(CypherParser.T__6) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ImplicitProcedureInvocationContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_ProcedureName(self): + return self.getTypedRuleContext(CypherParser.OC_ProcedureNameContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ImplicitProcedureInvocation + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ImplicitProcedureInvocation" ): + listener.enterOC_ImplicitProcedureInvocation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ImplicitProcedureInvocation" ): + listener.exitOC_ImplicitProcedureInvocation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ImplicitProcedureInvocation" ): + return visitor.visitOC_ImplicitProcedureInvocation(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ImplicitProcedureInvocation(self): + + localctx = CypherParser.OC_ImplicitProcedureInvocationContext(self, self._ctx, self.state) + self.enterRule(localctx, 166, self.RULE_oC_ImplicitProcedureInvocation) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1452 + self.oC_ProcedureName() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ProcedureResultFieldContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SymbolicName(self): + return self.getTypedRuleContext(CypherParser.OC_SymbolicNameContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ProcedureResultField + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ProcedureResultField" ): + listener.enterOC_ProcedureResultField(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ProcedureResultField" ): + listener.exitOC_ProcedureResultField(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ProcedureResultField" ): + return visitor.visitOC_ProcedureResultField(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ProcedureResultField(self): + + localctx = CypherParser.OC_ProcedureResultFieldContext(self, self._ctx, self.state) + self.enterRule(localctx, 168, self.RULE_oC_ProcedureResultField) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1454 + self.oC_SymbolicName() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ProcedureNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_Namespace(self): + return self.getTypedRuleContext(CypherParser.OC_NamespaceContext,0) + + + def oC_SymbolicName(self): + return self.getTypedRuleContext(CypherParser.OC_SymbolicNameContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ProcedureName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ProcedureName" ): + listener.enterOC_ProcedureName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ProcedureName" ): + listener.exitOC_ProcedureName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ProcedureName" ): + return visitor.visitOC_ProcedureName(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ProcedureName(self): + + localctx = CypherParser.OC_ProcedureNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 170, self.RULE_oC_ProcedureName) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1456 + self.oC_Namespace() + self.state = 1457 + self.oC_SymbolicName() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_NamespaceContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SymbolicName(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_SymbolicNameContext) + else: + return self.getTypedRuleContext(CypherParser.OC_SymbolicNameContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Namespace + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Namespace" ): + listener.enterOC_Namespace(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Namespace" ): + listener.exitOC_Namespace(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Namespace" ): + return visitor.visitOC_Namespace(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Namespace(self): + + localctx = CypherParser.OC_NamespaceContext(self, self._ctx, self.state) + self.enterRule(localctx, 172, self.RULE_oC_Namespace) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1464 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,270,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1459 + self.oC_SymbolicName() + self.state = 1460 + self.match(CypherParser.T__22) + self.state = 1466 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,270,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_VariableContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SymbolicName(self): + return self.getTypedRuleContext(CypherParser.OC_SymbolicNameContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Variable + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Variable" ): + listener.enterOC_Variable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Variable" ): + listener.exitOC_Variable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Variable" ): + return visitor.visitOC_Variable(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Variable(self): + + localctx = CypherParser.OC_VariableContext(self, self._ctx, self.state) + self.enterRule(localctx, 174, self.RULE_oC_Variable) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1467 + self.oC_SymbolicName() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_LiteralContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_BooleanLiteral(self): + return self.getTypedRuleContext(CypherParser.OC_BooleanLiteralContext,0) + + + def NULL(self): + return self.getToken(CypherParser.NULL, 0) + + def oC_NumberLiteral(self): + return self.getTypedRuleContext(CypherParser.OC_NumberLiteralContext,0) + + + def StringLiteral(self): + return self.getToken(CypherParser.StringLiteral, 0) + + def oC_ListLiteral(self): + return self.getTypedRuleContext(CypherParser.OC_ListLiteralContext,0) + + + def oC_MapLiteral(self): + return self.getTypedRuleContext(CypherParser.OC_MapLiteralContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Literal + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Literal" ): + listener.enterOC_Literal(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Literal" ): + listener.exitOC_Literal(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Literal" ): + return visitor.visitOC_Literal(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Literal(self): + + localctx = CypherParser.OC_LiteralContext(self, self._ctx, self.state) + self.enterRule(localctx, 176, self.RULE_oC_Literal) + try: + self.state = 1475 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [93, 94]: + self.enterOuterAlt(localctx, 1) + self.state = 1469 + self.oC_BooleanLiteral() + pass + elif token in [82]: + self.enterOuterAlt(localctx, 2) + self.state = 1470 + self.match(CypherParser.NULL) + pass + elif token in [95, 96, 97, 105, 106]: + self.enterOuterAlt(localctx, 3) + self.state = 1471 + self.oC_NumberLiteral() + pass + elif token in [107]: + self.enterOuterAlt(localctx, 4) + self.state = 1472 + self.match(CypherParser.StringLiteral) + pass + elif token in [8]: + self.enterOuterAlt(localctx, 5) + self.state = 1473 + self.oC_ListLiteral() + pass + elif token in [24]: + self.enterOuterAlt(localctx, 6) + self.state = 1474 + self.oC_MapLiteral() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_BooleanLiteralContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def TRUE(self): + return self.getToken(CypherParser.TRUE, 0) + + def FALSE(self): + return self.getToken(CypherParser.FALSE, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_BooleanLiteral + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_BooleanLiteral" ): + listener.enterOC_BooleanLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_BooleanLiteral" ): + listener.exitOC_BooleanLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_BooleanLiteral" ): + return visitor.visitOC_BooleanLiteral(self) + else: + return visitor.visitChildren(self) + + + + + def oC_BooleanLiteral(self): + + localctx = CypherParser.OC_BooleanLiteralContext(self, self._ctx, self.state) + self.enterRule(localctx, 178, self.RULE_oC_BooleanLiteral) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1477 + _la = self._input.LA(1) + if not(_la==93 or _la==94): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_NumberLiteralContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_DoubleLiteral(self): + return self.getTypedRuleContext(CypherParser.OC_DoubleLiteralContext,0) + + + def oC_IntegerLiteral(self): + return self.getTypedRuleContext(CypherParser.OC_IntegerLiteralContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_NumberLiteral + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_NumberLiteral" ): + listener.enterOC_NumberLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_NumberLiteral" ): + listener.exitOC_NumberLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_NumberLiteral" ): + return visitor.visitOC_NumberLiteral(self) + else: + return visitor.visitChildren(self) + + + + + def oC_NumberLiteral(self): + + localctx = CypherParser.OC_NumberLiteralContext(self, self._ctx, self.state) + self.enterRule(localctx, 180, self.RULE_oC_NumberLiteral) + try: + self.state = 1481 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [105, 106]: + self.enterOuterAlt(localctx, 1) + self.state = 1479 + self.oC_DoubleLiteral() + pass + elif token in [95, 96, 97]: + self.enterOuterAlt(localctx, 2) + self.state = 1480 + self.oC_IntegerLiteral() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_IntegerLiteralContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def HexInteger(self): + return self.getToken(CypherParser.HexInteger, 0) + + def OctalInteger(self): + return self.getToken(CypherParser.OctalInteger, 0) + + def DecimalInteger(self): + return self.getToken(CypherParser.DecimalInteger, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_IntegerLiteral + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_IntegerLiteral" ): + listener.enterOC_IntegerLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_IntegerLiteral" ): + listener.exitOC_IntegerLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_IntegerLiteral" ): + return visitor.visitOC_IntegerLiteral(self) + else: + return visitor.visitChildren(self) + + + + + def oC_IntegerLiteral(self): + + localctx = CypherParser.OC_IntegerLiteralContext(self, self._ctx, self.state) + self.enterRule(localctx, 182, self.RULE_oC_IntegerLiteral) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1483 + _la = self._input.LA(1) + if not(((((_la - 95)) & ~0x3f) == 0 and ((1 << (_la - 95)) & 7) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_DoubleLiteralContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ExponentDecimalReal(self): + return self.getToken(CypherParser.ExponentDecimalReal, 0) + + def RegularDecimalReal(self): + return self.getToken(CypherParser.RegularDecimalReal, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_DoubleLiteral + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_DoubleLiteral" ): + listener.enterOC_DoubleLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_DoubleLiteral" ): + listener.exitOC_DoubleLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_DoubleLiteral" ): + return visitor.visitOC_DoubleLiteral(self) + else: + return visitor.visitChildren(self) + + + + + def oC_DoubleLiteral(self): + + localctx = CypherParser.OC_DoubleLiteralContext(self, self._ctx, self.state) + self.enterRule(localctx, 184, self.RULE_oC_DoubleLiteral) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1485 + _la = self._input.LA(1) + if not(_la==105 or _la==106): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ListLiteralContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_Expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_ListLiteral + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ListLiteral" ): + listener.enterOC_ListLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ListLiteral" ): + listener.exitOC_ListLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ListLiteral" ): + return visitor.visitOC_ListLiteral(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ListLiteral(self): + + localctx = CypherParser.OC_ListLiteralContext(self, self._ctx, self.state) + self.enterRule(localctx, 186, self.RULE_oC_ListLiteral) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1487 + self.match(CypherParser.T__7) + self.state = 1489 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1488 + self.match(CypherParser.SP) + + + self.state = 1508 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & 140737573028160) != 0) or ((((_la - 76)) & ~0x3f) == 0 and ((1 << (_la - 76)) & 343051394343361) != 0): + self.state = 1491 + self.oC_Expression() + self.state = 1493 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1492 + self.match(CypherParser.SP) + + + self.state = 1505 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==2: + self.state = 1495 + self.match(CypherParser.T__1) + self.state = 1497 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1496 + self.match(CypherParser.SP) + + + self.state = 1499 + self.oC_Expression() + self.state = 1501 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1500 + self.match(CypherParser.SP) + + + self.state = 1507 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 1510 + self.match(CypherParser.T__8) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_MapLiteralContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SP(self, i:int=None): + if i is None: + return self.getTokens(CypherParser.SP) + else: + return self.getToken(CypherParser.SP, i) + + def oC_PropertyKeyName(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_PropertyKeyNameContext) + else: + return self.getTypedRuleContext(CypherParser.OC_PropertyKeyNameContext,i) + + + def oC_Expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(CypherParser.OC_ExpressionContext) + else: + return self.getTypedRuleContext(CypherParser.OC_ExpressionContext,i) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_MapLiteral + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_MapLiteral" ): + listener.enterOC_MapLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_MapLiteral" ): + listener.exitOC_MapLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_MapLiteral" ): + return visitor.visitOC_MapLiteral(self) + else: + return visitor.visitChildren(self) + + + + + def oC_MapLiteral(self): + + localctx = CypherParser.OC_MapLiteralContext(self, self._ctx, self.state) + self.enterRule(localctx, 188, self.RULE_oC_MapLiteral) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1512 + self.match(CypherParser.T__23) + self.state = 1514 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1513 + self.match(CypherParser.SP) + + + self.state = 1549 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1729452625654448128) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 1441116715713822719) != 0): + self.state = 1516 + self.oC_PropertyKeyName() + self.state = 1518 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1517 + self.match(CypherParser.SP) + + + self.state = 1520 + self.match(CypherParser.T__9) + self.state = 1522 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1521 + self.match(CypherParser.SP) + + + self.state = 1524 + self.oC_Expression() + self.state = 1526 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1525 + self.match(CypherParser.SP) + + + self.state = 1546 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==2: + self.state = 1528 + self.match(CypherParser.T__1) + self.state = 1530 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1529 + self.match(CypherParser.SP) + + + self.state = 1532 + self.oC_PropertyKeyName() + self.state = 1534 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1533 + self.match(CypherParser.SP) + + + self.state = 1536 + self.match(CypherParser.T__9) + self.state = 1538 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1537 + self.match(CypherParser.SP) + + + self.state = 1540 + self.oC_Expression() + self.state = 1542 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==125: + self.state = 1541 + self.match(CypherParser.SP) + + + self.state = 1548 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 1551 + self.match(CypherParser.T__24) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_PropertyKeyNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SchemaName(self): + return self.getTypedRuleContext(CypherParser.OC_SchemaNameContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_PropertyKeyName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_PropertyKeyName" ): + listener.enterOC_PropertyKeyName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_PropertyKeyName" ): + listener.exitOC_PropertyKeyName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_PropertyKeyName" ): + return visitor.visitOC_PropertyKeyName(self) + else: + return visitor.visitChildren(self) + + + + + def oC_PropertyKeyName(self): + + localctx = CypherParser.OC_PropertyKeyNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 190, self.RULE_oC_PropertyKeyName) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1553 + self.oC_SchemaName() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ParameterContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SymbolicName(self): + return self.getTypedRuleContext(CypherParser.OC_SymbolicNameContext,0) + + + def DecimalInteger(self): + return self.getToken(CypherParser.DecimalInteger, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_Parameter + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Parameter" ): + listener.enterOC_Parameter(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Parameter" ): + listener.exitOC_Parameter(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Parameter" ): + return visitor.visitOC_Parameter(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Parameter(self): + + localctx = CypherParser.OC_ParameterContext(self, self._ctx, self.state) + self.enterRule(localctx, 192, self.RULE_oC_Parameter) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1555 + self.match(CypherParser.T__25) + self.state = 1558 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [83, 89, 90, 91, 98, 119, 120, 121, 124]: + self.state = 1556 + self.oC_SymbolicName() + pass + elif token in [96]: + self.state = 1557 + self.match(CypherParser.DecimalInteger) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_SchemaNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def oC_SymbolicName(self): + return self.getTypedRuleContext(CypherParser.OC_SymbolicNameContext,0) + + + def oC_ReservedWord(self): + return self.getTypedRuleContext(CypherParser.OC_ReservedWordContext,0) + + + def getRuleIndex(self): + return CypherParser.RULE_oC_SchemaName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_SchemaName" ): + listener.enterOC_SchemaName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_SchemaName" ): + listener.exitOC_SchemaName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_SchemaName" ): + return visitor.visitOC_SchemaName(self) + else: + return visitor.visitChildren(self) + + + + + def oC_SchemaName(self): + + localctx = CypherParser.OC_SchemaNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 194, self.RULE_oC_SchemaName) + try: + self.state = 1562 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [83, 89, 90, 91, 98, 119, 120, 121, 124]: + self.enterOuterAlt(localctx, 1) + self.state = 1560 + self.oC_SymbolicName() + pass + elif token in [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 92, 93, 94, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118]: + self.enterOuterAlt(localctx, 2) + self.state = 1561 + self.oC_ReservedWord() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_ReservedWordContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ALL(self): + return self.getToken(CypherParser.ALL, 0) + + def ASC(self): + return self.getToken(CypherParser.ASC, 0) + + def ASCENDING(self): + return self.getToken(CypherParser.ASCENDING, 0) + + def BY(self): + return self.getToken(CypherParser.BY, 0) + + def CREATE(self): + return self.getToken(CypherParser.CREATE, 0) + + def DELETE(self): + return self.getToken(CypherParser.DELETE, 0) + + def DESC(self): + return self.getToken(CypherParser.DESC, 0) + + def DESCENDING(self): + return self.getToken(CypherParser.DESCENDING, 0) + + def DETACH(self): + return self.getToken(CypherParser.DETACH, 0) + + def EXISTS(self): + return self.getToken(CypherParser.EXISTS, 0) + + def LIMIT(self): + return self.getToken(CypherParser.LIMIT, 0) + + def MATCH(self): + return self.getToken(CypherParser.MATCH, 0) + + def MERGE(self): + return self.getToken(CypherParser.MERGE, 0) + + def ON(self): + return self.getToken(CypherParser.ON, 0) + + def OPTIONAL(self): + return self.getToken(CypherParser.OPTIONAL, 0) + + def ORDER(self): + return self.getToken(CypherParser.ORDER, 0) + + def REMOVE(self): + return self.getToken(CypherParser.REMOVE, 0) + + def RETURN(self): + return self.getToken(CypherParser.RETURN, 0) + + def SET(self): + return self.getToken(CypherParser.SET, 0) + + def L_SKIP(self): + return self.getToken(CypherParser.L_SKIP, 0) + + def WHERE(self): + return self.getToken(CypherParser.WHERE, 0) + + def WITH(self): + return self.getToken(CypherParser.WITH, 0) + + def UNION(self): + return self.getToken(CypherParser.UNION, 0) + + def UNWIND(self): + return self.getToken(CypherParser.UNWIND, 0) + + def AND(self): + return self.getToken(CypherParser.AND, 0) + + def AS(self): + return self.getToken(CypherParser.AS, 0) + + def CONTAINS(self): + return self.getToken(CypherParser.CONTAINS, 0) + + def DISTINCT(self): + return self.getToken(CypherParser.DISTINCT, 0) + + def ENDS(self): + return self.getToken(CypherParser.ENDS, 0) + + def IN(self): + return self.getToken(CypherParser.IN, 0) + + def IS(self): + return self.getToken(CypherParser.IS, 0) + + def NOT(self): + return self.getToken(CypherParser.NOT, 0) + + def OR(self): + return self.getToken(CypherParser.OR, 0) + + def STARTS(self): + return self.getToken(CypherParser.STARTS, 0) + + def XOR(self): + return self.getToken(CypherParser.XOR, 0) + + def FALSE(self): + return self.getToken(CypherParser.FALSE, 0) + + def TRUE(self): + return self.getToken(CypherParser.TRUE, 0) + + def NULL(self): + return self.getToken(CypherParser.NULL, 0) + + def CONSTRAINT(self): + return self.getToken(CypherParser.CONSTRAINT, 0) + + def DO(self): + return self.getToken(CypherParser.DO, 0) + + def FOR(self): + return self.getToken(CypherParser.FOR, 0) + + def REQUIRE(self): + return self.getToken(CypherParser.REQUIRE, 0) + + def UNIQUE(self): + return self.getToken(CypherParser.UNIQUE, 0) + + def CASE(self): + return self.getToken(CypherParser.CASE, 0) + + def WHEN(self): + return self.getToken(CypherParser.WHEN, 0) + + def THEN(self): + return self.getToken(CypherParser.THEN, 0) + + def ELSE(self): + return self.getToken(CypherParser.ELSE, 0) + + def END(self): + return self.getToken(CypherParser.END, 0) + + def MANDATORY(self): + return self.getToken(CypherParser.MANDATORY, 0) + + def SCALAR(self): + return self.getToken(CypherParser.SCALAR, 0) + + def OF(self): + return self.getToken(CypherParser.OF, 0) + + def ADD(self): + return self.getToken(CypherParser.ADD, 0) + + def DROP(self): + return self.getToken(CypherParser.DROP, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_ReservedWord + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_ReservedWord" ): + listener.enterOC_ReservedWord(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_ReservedWord" ): + listener.exitOC_ReservedWord(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_ReservedWord" ): + return visitor.visitOC_ReservedWord(self) + else: + return visitor.visitChildren(self) + + + + + def oC_ReservedWord(self): + + localctx = CypherParser.OC_ReservedWordContext(self, self._ctx, self.state) + self.enterRule(localctx, 196, self.RULE_oC_ReservedWord) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1564 + _la = self._input.LA(1) + if not((((_la) & ~0x3f) == 0 and ((1 << _la) & -1729452625654448128) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 35993614558953471) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_SymbolicNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def UnescapedSymbolicName(self): + return self.getToken(CypherParser.UnescapedSymbolicName, 0) + + def EscapedSymbolicName(self): + return self.getToken(CypherParser.EscapedSymbolicName, 0) + + def HexLetter(self): + return self.getToken(CypherParser.HexLetter, 0) + + def COUNT(self): + return self.getToken(CypherParser.COUNT, 0) + + def FILTER(self): + return self.getToken(CypherParser.FILTER, 0) + + def EXTRACT(self): + return self.getToken(CypherParser.EXTRACT, 0) + + def ANY(self): + return self.getToken(CypherParser.ANY, 0) + + def NONE(self): + return self.getToken(CypherParser.NONE, 0) + + def SINGLE(self): + return self.getToken(CypherParser.SINGLE, 0) + + def getRuleIndex(self): + return CypherParser.RULE_oC_SymbolicName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_SymbolicName" ): + listener.enterOC_SymbolicName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_SymbolicName" ): + listener.exitOC_SymbolicName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_SymbolicName" ): + return visitor.visitOC_SymbolicName(self) + else: + return visitor.visitChildren(self) + + + + + def oC_SymbolicName(self): + + localctx = CypherParser.OC_SymbolicNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 198, self.RULE_oC_SymbolicName) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1566 + _la = self._input.LA(1) + if not(((((_la - 83)) & ~0x3f) == 0 and ((1 << (_la - 83)) & 2680059625921) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_LeftArrowHeadContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return CypherParser.RULE_oC_LeftArrowHead + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_LeftArrowHead" ): + listener.enterOC_LeftArrowHead(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_LeftArrowHead" ): + listener.exitOC_LeftArrowHead(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_LeftArrowHead" ): + return visitor.visitOC_LeftArrowHead(self) + else: + return visitor.visitChildren(self) + + + + + def oC_LeftArrowHead(self): + + localctx = CypherParser.OC_LeftArrowHeadContext(self, self._ctx, self.state) + self.enterRule(localctx, 200, self.RULE_oC_LeftArrowHead) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1568 + _la = self._input.LA(1) + if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 2013282304) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_RightArrowHeadContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return CypherParser.RULE_oC_RightArrowHead + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_RightArrowHead" ): + listener.enterOC_RightArrowHead(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_RightArrowHead" ): + listener.exitOC_RightArrowHead(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_RightArrowHead" ): + return visitor.visitOC_RightArrowHead(self) + else: + return visitor.visitChildren(self) + + + + + def oC_RightArrowHead(self): + + localctx = CypherParser.OC_RightArrowHeadContext(self, self._ctx, self.state) + self.enterRule(localctx, 202, self.RULE_oC_RightArrowHead) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1570 + _la = self._input.LA(1) + if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 32212287488) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OC_DashContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return CypherParser.RULE_oC_Dash + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOC_Dash" ): + listener.enterOC_Dash(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOC_Dash" ): + listener.exitOC_Dash(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOC_Dash" ): + return visitor.visitOC_Dash(self) + else: + return visitor.visitChildren(self) + + + + + def oC_Dash(self): + + localctx = CypherParser.OC_DashContext(self, self._ctx, self.state) + self.enterRule(localctx, 204, self.RULE_oC_Dash) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1572 + _la = self._input.LA(1) + if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 70334384963584) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + + + diff --git a/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherVisitor.py b/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherVisitor.py new file mode 100644 index 000000000..27a7239dc --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/_cypher/gen/CypherVisitor.py @@ -0,0 +1,528 @@ +# Generated from Cypher.g4 by ANTLR 4.13.2 +from antlr4 import * +if "." in __name__: + from .CypherParser import CypherParser +else: + from CypherParser import CypherParser + +# This class defines a complete generic visitor for a parse tree produced by CypherParser. + +class CypherVisitor(ParseTreeVisitor): + + # Visit a parse tree produced by CypherParser#oC_Cypher. + def visitOC_Cypher(self, ctx:CypherParser.OC_CypherContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Statement. + def visitOC_Statement(self, ctx:CypherParser.OC_StatementContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Query. + def visitOC_Query(self, ctx:CypherParser.OC_QueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_RegularQuery. + def visitOC_RegularQuery(self, ctx:CypherParser.OC_RegularQueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Union. + def visitOC_Union(self, ctx:CypherParser.OC_UnionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_SingleQuery. + def visitOC_SingleQuery(self, ctx:CypherParser.OC_SingleQueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_SinglePartQuery. + def visitOC_SinglePartQuery(self, ctx:CypherParser.OC_SinglePartQueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_MultiPartQuery. + def visitOC_MultiPartQuery(self, ctx:CypherParser.OC_MultiPartQueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_UpdatingClause. + def visitOC_UpdatingClause(self, ctx:CypherParser.OC_UpdatingClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ReadingClause. + def visitOC_ReadingClause(self, ctx:CypherParser.OC_ReadingClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Match. + def visitOC_Match(self, ctx:CypherParser.OC_MatchContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Unwind. + def visitOC_Unwind(self, ctx:CypherParser.OC_UnwindContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Merge. + def visitOC_Merge(self, ctx:CypherParser.OC_MergeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_MergeAction. + def visitOC_MergeAction(self, ctx:CypherParser.OC_MergeActionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Create. + def visitOC_Create(self, ctx:CypherParser.OC_CreateContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Set. + def visitOC_Set(self, ctx:CypherParser.OC_SetContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_SetItem. + def visitOC_SetItem(self, ctx:CypherParser.OC_SetItemContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Delete. + def visitOC_Delete(self, ctx:CypherParser.OC_DeleteContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Remove. + def visitOC_Remove(self, ctx:CypherParser.OC_RemoveContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_RemoveItem. + def visitOC_RemoveItem(self, ctx:CypherParser.OC_RemoveItemContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_InQueryCall. + def visitOC_InQueryCall(self, ctx:CypherParser.OC_InQueryCallContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_StandaloneCall. + def visitOC_StandaloneCall(self, ctx:CypherParser.OC_StandaloneCallContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_YieldItems. + def visitOC_YieldItems(self, ctx:CypherParser.OC_YieldItemsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_YieldItem. + def visitOC_YieldItem(self, ctx:CypherParser.OC_YieldItemContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_With. + def visitOC_With(self, ctx:CypherParser.OC_WithContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Return. + def visitOC_Return(self, ctx:CypherParser.OC_ReturnContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ProjectionBody. + def visitOC_ProjectionBody(self, ctx:CypherParser.OC_ProjectionBodyContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ProjectionItems. + def visitOC_ProjectionItems(self, ctx:CypherParser.OC_ProjectionItemsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ProjectionItem. + def visitOC_ProjectionItem(self, ctx:CypherParser.OC_ProjectionItemContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Order. + def visitOC_Order(self, ctx:CypherParser.OC_OrderContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Skip. + def visitOC_Skip(self, ctx:CypherParser.OC_SkipContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Limit. + def visitOC_Limit(self, ctx:CypherParser.OC_LimitContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_SortItem. + def visitOC_SortItem(self, ctx:CypherParser.OC_SortItemContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Where. + def visitOC_Where(self, ctx:CypherParser.OC_WhereContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Pattern. + def visitOC_Pattern(self, ctx:CypherParser.OC_PatternContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_PatternPart. + def visitOC_PatternPart(self, ctx:CypherParser.OC_PatternPartContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_AnonymousPatternPart. + def visitOC_AnonymousPatternPart(self, ctx:CypherParser.OC_AnonymousPatternPartContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_PatternElement. + def visitOC_PatternElement(self, ctx:CypherParser.OC_PatternElementContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_RelationshipsPattern. + def visitOC_RelationshipsPattern(self, ctx:CypherParser.OC_RelationshipsPatternContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_NodePattern. + def visitOC_NodePattern(self, ctx:CypherParser.OC_NodePatternContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_PatternElementChain. + def visitOC_PatternElementChain(self, ctx:CypherParser.OC_PatternElementChainContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_RelationshipPattern. + def visitOC_RelationshipPattern(self, ctx:CypherParser.OC_RelationshipPatternContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_RelationshipDetail. + def visitOC_RelationshipDetail(self, ctx:CypherParser.OC_RelationshipDetailContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Properties. + def visitOC_Properties(self, ctx:CypherParser.OC_PropertiesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_RelationshipTypes. + def visitOC_RelationshipTypes(self, ctx:CypherParser.OC_RelationshipTypesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_NodeLabels. + def visitOC_NodeLabels(self, ctx:CypherParser.OC_NodeLabelsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_NodeLabel. + def visitOC_NodeLabel(self, ctx:CypherParser.OC_NodeLabelContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_RangeLiteral. + def visitOC_RangeLiteral(self, ctx:CypherParser.OC_RangeLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_LabelName. + def visitOC_LabelName(self, ctx:CypherParser.OC_LabelNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_RelTypeName. + def visitOC_RelTypeName(self, ctx:CypherParser.OC_RelTypeNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_PropertyExpression. + def visitOC_PropertyExpression(self, ctx:CypherParser.OC_PropertyExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Expression. + def visitOC_Expression(self, ctx:CypherParser.OC_ExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_OrExpression. + def visitOC_OrExpression(self, ctx:CypherParser.OC_OrExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_XorExpression. + def visitOC_XorExpression(self, ctx:CypherParser.OC_XorExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_AndExpression. + def visitOC_AndExpression(self, ctx:CypherParser.OC_AndExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_NotExpression. + def visitOC_NotExpression(self, ctx:CypherParser.OC_NotExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ComparisonExpression. + def visitOC_ComparisonExpression(self, ctx:CypherParser.OC_ComparisonExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_PartialComparisonExpression. + def visitOC_PartialComparisonExpression(self, ctx:CypherParser.OC_PartialComparisonExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_StringListNullPredicateExpression. + def visitOC_StringListNullPredicateExpression(self, ctx:CypherParser.OC_StringListNullPredicateExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_StringPredicateExpression. + def visitOC_StringPredicateExpression(self, ctx:CypherParser.OC_StringPredicateExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ListPredicateExpression. + def visitOC_ListPredicateExpression(self, ctx:CypherParser.OC_ListPredicateExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_NullPredicateExpression. + def visitOC_NullPredicateExpression(self, ctx:CypherParser.OC_NullPredicateExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_AddOrSubtractExpression. + def visitOC_AddOrSubtractExpression(self, ctx:CypherParser.OC_AddOrSubtractExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_MultiplyDivideModuloExpression. + def visitOC_MultiplyDivideModuloExpression(self, ctx:CypherParser.OC_MultiplyDivideModuloExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_PowerOfExpression. + def visitOC_PowerOfExpression(self, ctx:CypherParser.OC_PowerOfExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_UnaryAddOrSubtractExpression. + def visitOC_UnaryAddOrSubtractExpression(self, ctx:CypherParser.OC_UnaryAddOrSubtractExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_NonArithmeticOperatorExpression. + def visitOC_NonArithmeticOperatorExpression(self, ctx:CypherParser.OC_NonArithmeticOperatorExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ListOperatorExpression. + def visitOC_ListOperatorExpression(self, ctx:CypherParser.OC_ListOperatorExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_PropertyLookup. + def visitOC_PropertyLookup(self, ctx:CypherParser.OC_PropertyLookupContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Atom. + def visitOC_Atom(self, ctx:CypherParser.OC_AtomContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_CaseExpression. + def visitOC_CaseExpression(self, ctx:CypherParser.OC_CaseExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_CaseAlternative. + def visitOC_CaseAlternative(self, ctx:CypherParser.OC_CaseAlternativeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ListComprehension. + def visitOC_ListComprehension(self, ctx:CypherParser.OC_ListComprehensionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_PatternComprehension. + def visitOC_PatternComprehension(self, ctx:CypherParser.OC_PatternComprehensionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Quantifier. + def visitOC_Quantifier(self, ctx:CypherParser.OC_QuantifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_FilterExpression. + def visitOC_FilterExpression(self, ctx:CypherParser.OC_FilterExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_PatternPredicate. + def visitOC_PatternPredicate(self, ctx:CypherParser.OC_PatternPredicateContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ParenthesizedExpression. + def visitOC_ParenthesizedExpression(self, ctx:CypherParser.OC_ParenthesizedExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_IdInColl. + def visitOC_IdInColl(self, ctx:CypherParser.OC_IdInCollContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_FunctionInvocation. + def visitOC_FunctionInvocation(self, ctx:CypherParser.OC_FunctionInvocationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_FunctionName. + def visitOC_FunctionName(self, ctx:CypherParser.OC_FunctionNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ExistentialSubquery. + def visitOC_ExistentialSubquery(self, ctx:CypherParser.OC_ExistentialSubqueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ExplicitProcedureInvocation. + def visitOC_ExplicitProcedureInvocation(self, ctx:CypherParser.OC_ExplicitProcedureInvocationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ImplicitProcedureInvocation. + def visitOC_ImplicitProcedureInvocation(self, ctx:CypherParser.OC_ImplicitProcedureInvocationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ProcedureResultField. + def visitOC_ProcedureResultField(self, ctx:CypherParser.OC_ProcedureResultFieldContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ProcedureName. + def visitOC_ProcedureName(self, ctx:CypherParser.OC_ProcedureNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Namespace. + def visitOC_Namespace(self, ctx:CypherParser.OC_NamespaceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Variable. + def visitOC_Variable(self, ctx:CypherParser.OC_VariableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Literal. + def visitOC_Literal(self, ctx:CypherParser.OC_LiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_BooleanLiteral. + def visitOC_BooleanLiteral(self, ctx:CypherParser.OC_BooleanLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_NumberLiteral. + def visitOC_NumberLiteral(self, ctx:CypherParser.OC_NumberLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_IntegerLiteral. + def visitOC_IntegerLiteral(self, ctx:CypherParser.OC_IntegerLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_DoubleLiteral. + def visitOC_DoubleLiteral(self, ctx:CypherParser.OC_DoubleLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ListLiteral. + def visitOC_ListLiteral(self, ctx:CypherParser.OC_ListLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_MapLiteral. + def visitOC_MapLiteral(self, ctx:CypherParser.OC_MapLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_PropertyKeyName. + def visitOC_PropertyKeyName(self, ctx:CypherParser.OC_PropertyKeyNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Parameter. + def visitOC_Parameter(self, ctx:CypherParser.OC_ParameterContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_SchemaName. + def visitOC_SchemaName(self, ctx:CypherParser.OC_SchemaNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_ReservedWord. + def visitOC_ReservedWord(self, ctx:CypherParser.OC_ReservedWordContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_SymbolicName. + def visitOC_SymbolicName(self, ctx:CypherParser.OC_SymbolicNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_LeftArrowHead. + def visitOC_LeftArrowHead(self, ctx:CypherParser.OC_LeftArrowHeadContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_RightArrowHead. + def visitOC_RightArrowHead(self, ctx:CypherParser.OC_RightArrowHeadContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by CypherParser#oC_Dash. + def visitOC_Dash(self, ctx:CypherParser.OC_DashContext): + return self.visitChildren(ctx) + + + +del CypherParser \ No newline at end of file diff --git a/packages/ai/src/ai/common/graph/age/_cypher/gen/__init__.py b/packages/ai/src/ai/common/graph/age/_cypher/gen/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/ai/src/ai/common/graph/age/analysis.py b/packages/ai/src/ai/common/graph/age/analysis.py new file mode 100644 index 000000000..eb73e605c --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/analysis.py @@ -0,0 +1,265 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Cypher analysis: openCypher ANTLR parse -> structured facts. + +This is the parse stage of the translation pipeline. It runs the vendored +openCypher M23 parser (see ``_cypher/``) over the query text and extracts the +facts every later stage needs: + +- the RETURN projection (column display names, or ``RETURN *`` / no RETURN), +- which write clauses appear (typed contexts, not regex), +- variable-length relationship ranges (for the firewall's depth cap), +- ``$param`` names, and invoked function names (for the capability table). + +Parsing failures raise :class:`~.errors.AgeTranslationError` with the +collected syntax messages — the same text the LLM repair loop feeds back. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import List, Optional, Set, Tuple + +from antlr4 import CommonTokenStream, InputStream, ParserRuleContext +from antlr4.error.ErrorListener import ErrorListener + +from ._cypher.gen.CypherLexer import CypherLexer +from ._cypher.gen.CypherParser import CypherParser +from .errors import AgeTranslationError + + +@dataclass +class ReturnColumn: + """One projection item of the final RETURN clause.""" + + # Name used to key decoded result rows: the AS alias when present, the + # bare variable/property text when simple, else a generated placeholder. + display_name: str + # Raw expression text (diagnostics only). + expression: str + # True when display_name came from an explicit AS alias. + is_alias: bool = False + + +@dataclass +class CypherFacts: + """Everything later pipeline stages need to know about a Cypher query.""" + + query: str + # None => the statement has no RETURN clause (pure write); the emitter + # synthesizes a single throwaway column in that case. + return_columns: Optional[List[ReturnColumn]] = None + returns_star: bool = False + write_clauses: Set[str] = field(default_factory=set) + has_call: bool = False + # (lower_bound, upper_bound) per variable-length pattern; None = unbounded. + var_length_ranges: List[Tuple[Optional[int], Optional[int]]] = field(default_factory=list) + param_names: Set[str] = field(default_factory=set) + function_names: Set[str] = field(default_factory=set) + # MERGE ... ON CREATE/ON MATCH actions (capability: merge_on_set). + has_merge_action: bool = False + # A label check used as an expression (e.g. WHERE (n:Label)) rather than in + # a pattern (capability: where_label_check). + has_where_label_check: bool = False + # A pattern node carrying more than one label (capability: multi_label). + has_multi_label: bool = False + # ORDER BY references a bare projection alias rather than an expression + # (capability: order_by_alias — AGE 1.5.0: 'could not find rte for '). + has_order_by_alias: bool = False + + @property + def is_write(self) -> bool: + return bool(self.write_clauses) + + +class _CollectingErrorListener(ErrorListener): + """Collect syntax errors instead of ANTLR's default stderr printing.""" + + def __init__(self) -> None: + self.errors: List[str] = [] + + def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): # noqa: N802 (ANTLR API) + self.errors.append(f'line {line}:{column} {msg}') + + +def _strip_backticks(name: str) -> str: + if len(name) >= 2 and name.startswith('`') and name.endswith('`'): + return name[1:-1].replace('``', '`') + return name + + +def _source_text(ctx: ParserRuleContext) -> str: + """Original source slice for a context (getText() drops whitespace).""" + stream = ctx.start.getInputStream() + return stream.getText(ctx.start.start, ctx.stop.stop) + + +def _walk(ctx, visit) -> None: + visit(ctx) + for i in range(ctx.getChildCount()): + child = ctx.getChild(i) + if child.getChildCount() or isinstance(child, ParserRuleContext): + _walk(child, visit) + else: + visit(child) + + +def _depth(ctx) -> int: + d = 0 + node = ctx + while node.parentCtx is not None: + node = node.parentCtx + d += 1 + return d + + +def _parse_range_literal(text: str) -> Tuple[Optional[int], Optional[int]]: + """``*``/``*3``/``*1..3``/``*..5``/``*2..`` -> (lower, upper), None=absent.""" + body = text.lstrip('*').replace(' ', '') + if not body: + return (None, None) + if '..' not in body: + n = int(body) + return (n, n) + low_s, _, high_s = body.partition('..') + return (int(low_s) if low_s else None, int(high_s) if high_s else None) + + +def _projection_column(item_ctx) -> ReturnColumn: + """Build a ReturnColumn from an oC_ProjectionItem context.""" + expression = _source_text(item_ctx) + # Grammar: oC_ProjectionItem : ( oC_Expression SP AS SP oC_Variable ) | oC_Expression ; + variable = item_ctx.oC_Variable() if hasattr(item_ctx, 'oC_Variable') else None + if variable is not None: + return ReturnColumn(display_name=_strip_backticks(variable.getText()), expression=expression, is_alias=True) + expr_text = item_ctx.oC_Expression().getText() + return ReturnColumn(display_name=_strip_backticks(expr_text), expression=expression) + + +def analyze(query: str) -> CypherFacts: + """Parse ``query`` with the openCypher grammar and extract translation facts. + + Raises: + AgeTranslationError: when the text is not syntactically valid Cypher. + """ + if not query or not query.strip(): + raise AgeTranslationError('Empty Cypher query') + + listener = _CollectingErrorListener() + lexer = CypherLexer(InputStream(query)) + lexer.removeErrorListeners() + lexer.addErrorListener(listener) + parser = CypherParser(CommonTokenStream(lexer)) + parser.removeErrorListeners() + parser.addErrorListener(listener) + + # The recursive-descent parse (and the walk below) burn stack per nesting + # level. The firewall's pre-parse nesting cap keeps normal input far from + # the limit; this backstop keeps the layer's contract — every failure is + # an AgeTranslationError — even for input that slips past the scan. + try: + tree = parser.oC_Cypher() + except RecursionError: + raise AgeTranslationError( + 'Cypher expression is nested too deeply to parse (reduce parenthesis/expression nesting)' + ) from None + if listener.errors: + raise AgeTranslationError('Cypher syntax error: ' + '; '.join(listener.errors[:5])) + + facts = CypherFacts(query=query) + + # Write clauses / CALLs / ranges / params / functions — a single walk. + write_map = { + CypherParser.OC_CreateContext: 'CREATE', + CypherParser.OC_MergeContext: 'MERGE', + CypherParser.OC_DeleteContext: 'DELETE', + CypherParser.OC_SetContext: 'SET', + CypherParser.OC_RemoveContext: 'REMOVE', + } + returns: List = [] + + def visit(node) -> None: + for ctx_cls, clause in write_map.items(): + if isinstance(node, ctx_cls): + facts.write_clauses.add(clause) + if isinstance(node, (CypherParser.OC_StandaloneCallContext, CypherParser.OC_InQueryCallContext)): + facts.has_call = True + if isinstance(node, CypherParser.OC_RangeLiteralContext): + facts.var_length_ranges.append(_parse_range_literal(node.getText())) + if isinstance(node, CypherParser.OC_ParameterContext): + facts.param_names.add(node.getText().lstrip('$')) + if isinstance(node, CypherParser.OC_FunctionInvocationContext): + name_ctx = node.oC_FunctionName() + if name_ctx is not None: + facts.function_names.add(name_ctx.getText().lower()) + if isinstance(node, CypherParser.OC_MergeActionContext): + facts.has_merge_action = True + if isinstance(node, CypherParser.OC_NodeLabelsContext): + if len(node.oC_NodeLabel()) > 1: + facts.has_multi_label = True + # A NodeLabels context under a WHERE (not inside a pattern) is a + # label check used as an expression: WHERE (n:Label). + parent = node.parentCtx + while parent is not None: + if isinstance(parent, CypherParser.OC_WhereContext): + facts.has_where_label_check = True + break + if isinstance(parent, CypherParser.OC_PatternContext): + break + parent = parent.parentCtx + if isinstance(node, CypherParser.OC_ReturnContext): + returns.append(node) + if isinstance(node, CypherParser.OC_SortItemContext): + sort_items.append(node.oC_Expression().getText()) + + sort_items: List[str] = [] + try: + _walk(tree, visit) + except RecursionError: + raise AgeTranslationError( + 'Cypher expression is nested too deeply to analyze (reduce parenthesis/expression nesting)' + ) from None + + if returns: + # Multiple RETURNs at the same (shallowest) depth = UNION branches; all + # branches must project the same column count, so the first shallowest + # RETURN defines the projection. Deeper RETURNs (subqueries) are not + # the statement's result shape. + top = min(returns, key=_depth) + items_ctx = top.oC_ProjectionBody().oC_ProjectionItems() + # Grammar: oC_ProjectionItems : ( '*' ... ) | ( oC_ProjectionItem ... ) ; + if items_ctx.getChild(0).getText() == '*': + facts.returns_star = True + else: + facts.return_columns = [_projection_column(item) for item in items_ctx.oC_ProjectionItem()] + + # ORDER BY a bare AS-alias (not an expression) — an AGE 1.5.0 wrinkle: + # verified to fail with 'could not find rte for '. + if facts.return_columns: + aliases = {c.display_name for c in facts.return_columns if c.is_alias} + bare = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$') + facts.has_order_by_alias = any(bare.fullmatch(item) and item in aliases for item in sort_items) + + return facts diff --git a/packages/ai/src/ai/common/graph/age/capabilities.py b/packages/ai/src/ai/common/graph/age/capabilities.py new file mode 100644 index 000000000..842cc78c7 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/capabilities.py @@ -0,0 +1,187 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Dialect capability table — the rewrite framework, keyed by AGE version. + +Apache AGE speaks a Cypher subset with version-specific gaps. Each capability +cell records how a given AGE version handles one feature: + +- ``SUPPORTED``: passes through untouched. +- ``EMULATE``: a rewrite hook transforms the query into something AGE runs + (framework only in v1 — no emulations are implemented yet). +- ``REJECT``: raise :class:`~.errors.AgeUnsupportedFeature` before touching + the database, with an actionable message. +- ``TBD``: not yet verified against the live version. TBD cells pass through + — if AGE cannot run the construct, its own error surfaces via EXPLAIN or + execution (which the LLM repair loop consumes). Verify each TBD cell + against the live instance and promote it to a real status. + +Cells marked verified below were confirmed empirically against a container on +the live pin (PG16 + AGE 1.5.0); see the layer README. +""" + +from __future__ import annotations + +import enum +from dataclasses import dataclass +from typing import Callable, Dict, Optional + +from .analysis import CypherFacts +from .errors import AgeUnsupportedFeature + + +class CellStatus(enum.Enum): + SUPPORTED = 'supported' + EMULATE = 'emulate' + REJECT = 'reject' + TBD = 'tbd' + + +@dataclass(frozen=True) +class Capability: + """One feature cell for one AGE version.""" + + feature: str + status: CellStatus + # Predicate: does this query use the feature? + detect: Callable[[CypherFacts], bool] + # Message detail for REJECT; verification note otherwise. + detail: str = '' + # EMULATE hook: CypherFacts -> rewritten Cypher text. v1: none implemented. + rewrite: Optional[Callable[[CypherFacts], str]] = None + + +def _uses_function(name: str) -> Callable[[CypherFacts], bool]: + return lambda facts: name in facts.function_names + + +# --------------------------------------------------------------------------- +# AGE 1.5.0 — what the RocketRide cloud runs +# --------------------------------------------------------------------------- + +AGE_1_5_0: Dict[str, Capability] = { + cap.feature: cap + for cap in ( + # --- verified against the live pin (see README / age-mechanics probes) --- + Capability( + feature='datetime_function', + status=CellStatus.REJECT, + detect=_uses_function('datetime'), + detail=( + 'AGE 1.5.0 has no datetime() (function ag_catalog.age_datetime does not exist); ' + 'store timestamps as ISO-8601 strings or epoch numbers instead' + ), + ), + Capability( + feature='return_star', + status=CellStatus.REJECT, + detect=lambda facts: facts.returns_star, + detail=( + "AGE requires an explicit result-column list and 'RETURN *' needs full scope " + 'analysis to synthesize one; list the columns explicitly (e.g. RETURN a, b)' + ), + ), + Capability( + feature='order_by_alias', + status=CellStatus.REJECT, + detect=lambda facts: facts.has_order_by_alias, + detail=( + "AGE 1.5.0 cannot ORDER BY a projection alias ('could not find rte for '); " + 'order by the expression itself (e.g. ORDER BY r.since, not ORDER BY since)' + ), + ), + # --- verified 2026-07-28 against the exact pin container (PG 16.14 + + # AGE 1.5.0): all four are syntax-level rejections ('syntax error at or + # near ...'), while plain MERGE on the same graph succeeds — so these + # are real 1.5.0 grammar gaps, not harness artifacts. --- + Capability( + feature='merge_on_set', + status=CellStatus.REJECT, + detect=lambda facts: facts.has_merge_action, + detail=( + "AGE 1.5.0 does not parse MERGE ... ON CREATE/ON MATCH SET (syntax error at 'ON'); " + 'use plain MERGE, then a separate SET clause (MERGE (n) ... SET n.prop = ...)' + ), + ), + Capability( + feature='where_label_check', + status=CellStatus.REJECT, + detect=lambda facts: facts.has_where_label_check, + detail=( + "AGE 1.5.0 does not parse label predicates in WHERE — 'WHERE n:Label' and " + "'WHERE (n:Label)' both fail (syntax error at ':'); put the label in the " + 'pattern instead (MATCH (n:Label))' + ), + ), + Capability( + feature='multi_label', + status=CellStatus.REJECT, + detect=lambda facts: facts.has_multi_label, + detail=( + 'AGE 1.5.0 does not parse multiple labels on one node — (n:A:B) fails in both ' + "CREATE and MATCH (syntax error at ':'); model the second label as a property " + 'or an edge to a category node' + ), + ), + Capability( + feature='shortest_path', + status=CellStatus.REJECT, + detect=_uses_function('shortestpath'), + detail=( + 'AGE 1.5.0 has no shortestPath() (syntax error); use a bounded variable-length ' + 'match (e.g. (a)-[*..3]-(b)) and rank by path length in the query' + ), + ), + ) +} + +#: Capability tables keyed by AGE version string. +CAPABILITY_TABLES: Dict[str, Dict[str, Capability]] = { + '1.5.0': AGE_1_5_0, +} + +DEFAULT_AGE_VERSION = '1.5.0' + + +def apply_capabilities(facts: CypherFacts, age_version: str = DEFAULT_AGE_VERSION) -> CypherFacts: + """Run the dialect gate: REJECT cells raise; EMULATE cells rewrite. + + Unknown versions fall back to the newest known table (closest behaviour + beats no gate at all). TBD and SUPPORTED cells pass through unchanged. + """ + table = CAPABILITY_TABLES.get(age_version) + if table is None: + table = CAPABILITY_TABLES[sorted(CAPABILITY_TABLES)[-1]] + + for cap in table.values(): + if not cap.detect(facts): + continue + if cap.status is CellStatus.REJECT: + raise AgeUnsupportedFeature(cap.feature, cap.detail) + if cap.status is CellStatus.EMULATE and cap.rewrite is not None: + # v1 ships the framework only; when emulations land they re-analyze + # the rewritten text so later stages see consistent facts. + from .analysis import analyze + + facts = analyze(cap.rewrite(facts)) + return facts diff --git a/packages/ai/src/ai/common/graph/age/decode.py b/packages/ai/src/ai/common/graph/age/decode.py new file mode 100644 index 000000000..0d0277877 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/decode.py @@ -0,0 +1,120 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""agtype decoding: AGE result text -> plain Python values. + +Wraps the vendored Apache AGE driver decoder (``_agtype/``, ANTLR-based — +handles the full agtype superset incl. ``::vertex``/``::edge``/``::path`` +annotations, nested containers, and numeric edge cases that ``json.loads`` +cannot). Graph entities are flattened to plain dicts so rows survive the node +layer's JSON sanitisation: + +- vertex -> ``{'id', 'label', 'properties'}`` +- edge -> ``{'id', 'label', 'start_id', 'end_id', 'properties'}`` +- path -> list of those, in traversal order +""" + +from __future__ import annotations + +from decimal import Decimal +from typing import Any + +from antlr4 import CommonTokenStream, InputStream +from antlr4.error.ErrorListener import ErrorListener + +from ._agtype import Edge, Path, Vertex +from ._agtype.builder import ResultVisitor +from ._agtype.gen.AgtypeLexer import AgtypeLexer +from ._agtype.gen.AgtypeParser import AgtypeParser +from .errors import AgeTranslationError + + +class _StrictErrorListener(ErrorListener): + """Fail loud on malformed agtype (the vendored default error-recovers silently).""" + + def __init__(self) -> None: + self.errors: list[str] = [] + + def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): # noqa: N802 (ANTLR API) + self.errors.append(f'line {line}:{column} {msg}') + + +def _parse_agtype_strict(text: str) -> Any: + """Parse agtype text with the vendored grammar, raising on syntax errors.""" + listener = _StrictErrorListener() + lexer = AgtypeLexer(InputStream(text)) + lexer.removeErrorListeners() + lexer.addErrorListener(listener) + parser = AgtypeParser(CommonTokenStream(lexer)) + parser.removeErrorListeners() + parser.addErrorListener(listener) + tree = parser.agType() + if listener.errors: + raise AgeTranslationError('agtype syntax error: ' + '; '.join(listener.errors[:3])) + return tree.accept(ResultVisitor(None)) + + +def _to_plain(value: Any) -> Any: + if isinstance(value, Vertex): + return { + 'id': value.id, + 'label': value.label, + 'properties': _to_plain(value.properties or {}), + } + if isinstance(value, Edge): + return { + 'id': value.id, + 'label': value.label, + 'start_id': getattr(value, 'start_id', None), + 'end_id': getattr(value, 'end_id', None), + 'properties': _to_plain(value.properties or {}), + } + if isinstance(value, Path): + return [_to_plain(entity) for entity in value] + if isinstance(value, Decimal): + # agtype numerics parse to Decimal; JSON layers downstream want floats. + return float(value) + if isinstance(value, dict): + return {k: _to_plain(v) for k, v in value.items()} + if isinstance(value, list): + return [_to_plain(v) for v in value] + return value + + +def decode_agtype(text: Any) -> Any: + """Decode one agtype column value into a plain Python value. + + psycopg2 returns agtype columns as ``str`` (no registered adapter); SQL + NULL arrives as ``None`` and passes through. + """ + if text is None: + return None + if not isinstance(text, str): + # Defensive: an adapter already produced a Python value. + return _to_plain(text) + try: + return _to_plain(_parse_agtype_strict(text)) + except AgeTranslationError: + raise + except Exception as e: + raise AgeTranslationError(f'Failed to decode agtype value: {e}') from e diff --git a/packages/ai/src/ai/common/graph/age/emit.py b/packages/ai/src/ai/common/graph/age/emit.py new file mode 100644 index 000000000..6c5c50e3b --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/emit.py @@ -0,0 +1,169 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""SQL emission: the ``cypher()`` envelope, column list, and bind plan. + +Facts verified against the live AGE 1.5.0 pin (see the layer README): + +- The envelope is ``SELECT * FROM cypher('', $tag$$tag$ [, $1]) + AS ()``. The AS-list column *count* must equal the RETURN + column count exactly; the *names* are arbitrary — we emit generated safe + identifiers (``c0..cN``) and key decoded rows by the Cypher display names. +- Statements without RETURN take a single synthesized column and yield 0 rows. +- The cypher() params argument MUST be a real prepared-statement parameter — + an inline ``'...'::agtype`` literal is rejected by AGE. With params we emit + ``PREPARE (agtype) AS ...`` / ``EXECUTE (%s::agtype)`` / + ``DEALLOCATE `` for one transaction (transaction-pooler safe; + a rolled-back transaction discards its prepared statements). +- The Cypher body is embedded via dollar-quoting; the tag is chosen to not + collide with the body so user text can never escape the envelope. + +The emitter is DB-driver-agnostic: it produces SQL strings with psycopg2 +(``%s``) placeholders plus the bind values; the node owns the connection. +""" + +from __future__ import annotations + +import json +import re +import uuid +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Tuple + +from .analysis import CypherFacts +from .errors import AgeTranslationError + +# AGE graph names: unquoted-identifier discipline, same as table names. +VALID_GRAPH_NAME = re.compile(r'^[A-Za-z_][A-Za-z0-9_]{0,62}$') + + +@dataclass +class TranslatedQuery: + """The executable plan for one Cypher query. + + ``statements`` run in order inside ONE transaction; the statement at + ``result_index`` produces the result rows (one agtype column per entry in + ``columns``). ``binds`` pair positionally with ``statements``. + """ + + columns: List[str] + statements: List[str] = field(default_factory=list) + binds: List[Tuple[Any, ...]] = field(default_factory=list) + result_index: int = 0 + # True when the statement set carries no RETURN (pure write): zero rows + # are expected and 'affected' semantics apply. + has_return: bool = True + # The caller must open the transaction READ ONLY (safe path). + read_only: bool = False + + +def _dollar_quote(body: str) -> str: + """Wrap ``body`` in a dollar-quote tag guaranteed absent from it.""" + tag = '$rr_cypher$' + counter = 0 + while tag in body: + counter += 1 + tag = f'$rr_cypher{counter}$' + return f'{tag}{body}{tag}' + + +def _params_to_agtype_json(params: Dict[str, Any]) -> str: + """Serialize the params map to the JSON text cast to agtype at bind time.""" + try: + return json.dumps(params) + except (TypeError, ValueError) as e: + raise AgeTranslationError(f'Cypher parameters are not JSON-serialisable: {e}') from e + + +def emit( + facts: CypherFacts, + graph_name: str, + params: Optional[Dict[str, Any]] = None, + limit: Optional[int] = None, + statement_timeout_ms: int = 30_000, + read_only: bool = False, +) -> TranslatedQuery: + """Build the transaction's statement list for one translated query. + + The caller wraps the statements in BEGIN/COMMIT (READ ONLY when + ``read_only``); AGE is preloaded server-side, so no ``LOAD`` is emitted. + """ + if not VALID_GRAPH_NAME.fullmatch(graph_name or ''): + raise AgeTranslationError( + f'Invalid graph name {graph_name!r}: must be a valid identifier ' + '(letters, digits, underscores; max 63 chars)' + ) + if params and not facts.param_names: + raise AgeTranslationError('Parameters were supplied but the Cypher query references no $parameters') + if facts.param_names: + missing = sorted(facts.param_names - set(params or {})) + if missing: + raise AgeTranslationError( + 'Cypher query references parameters with no supplied values: ' + + ', '.join(f'${name}' for name in missing) + ) + + columns = [c.display_name for c in facts.return_columns] if facts.return_columns else [] + has_return = facts.return_columns is not None + # Generated safe identifiers; count is what AGE checks, names are ours. + as_list = ', '.join(f'c{i} agtype' for i in range(len(columns))) if has_return else 'v agtype' + + body = _dollar_quote(facts.query) + tq = TranslatedQuery(columns=columns, has_return=has_return) + + # Session discipline for the transaction-pooled cloud endpoint: LOCAL only. + tq.statements.append('SET LOCAL search_path = ag_catalog,"$user",public') + tq.binds.append(()) + timeout = max(1, int(statement_timeout_ms)) + tq.statements.append(f"SET LOCAL statement_timeout = '{timeout}ms'") + tq.binds.append(()) + + outer_limit = '' + if limit is not None: + outer_limit = f' LIMIT {max(0, int(limit))}' + + # Key the branch on the query's $parameters, not on bool(params): the + # missing-value check above guarantees params covers param_names here. + if facts.param_names: + # cypher()'s params argument must be a prepared-statement parameter. + stmt_name = f'_rr_age_{uuid.uuid4().hex[:12]}' + select = ( + f'PREPARE {stmt_name}(agtype) AS ' + f"SELECT * FROM cypher('{graph_name}', {body}, $1) AS ({as_list}){outer_limit}" + ) + tq.statements.append(select) + tq.binds.append(()) + tq.statements.append(f'EXECUTE {stmt_name}(%s::agtype)') + tq.binds.append((_params_to_agtype_json(params),)) + tq.result_index = len(tq.statements) - 1 + tq.statements.append(f'DEALLOCATE {stmt_name}') + tq.binds.append(()) + else: + select = f"SELECT * FROM cypher('{graph_name}', {body}) AS ({as_list}){outer_limit}" + tq.statements.append(select) + tq.binds.append(()) + tq.result_index = len(tq.statements) - 1 + + # read_only is enforced by the caller's BEGIN READ ONLY; carried for tests. + tq.read_only = read_only + return tq diff --git a/packages/ai/src/ai/common/graph/age/errors.py b/packages/ai/src/ai/common/graph/age/errors.py new file mode 100644 index 000000000..9cc82510c --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/errors.py @@ -0,0 +1,54 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Error taxonomy for the Cypher -> Apache AGE translation layer. + +All three fail loud (per the design): callers surface the message to the +LLM-repair loop or to the user rather than degrading silently. +""" + + +class AgeTranslationError(Exception): + """The Cypher text could not be translated (e.g. it does not parse).""" + + +class AgeUnsupportedFeature(AgeTranslationError): + """The Cypher uses a feature this AGE version cannot run (capability REJECT).""" + + def __init__(self, feature: str, detail: str = ''): + self.feature = feature + message = f'Unsupported on Apache AGE: {feature}' + if detail: + message += f' — {detail}' + super().__init__(message) + + +class AgeFirewallRejected(AgeTranslationError): + """The query violates a firewall rule (resource cap or write on the safe path).""" + + def __init__(self, rule: str, detail: str = ''): + self.rule = rule + message = f'Query rejected by firewall ({rule})' + if detail: + message += f': {detail}' + super().__init__(message) diff --git a/packages/ai/src/ai/common/graph/age/firewall.py b/packages/ai/src/ai/common/graph/age/firewall.py new file mode 100644 index 000000000..bb567ccc5 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/firewall.py @@ -0,0 +1,135 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Query firewall for the Cypher -> AGE pipeline. + +Two rule families, applied per the design: + +- **Resource caps** — enforced on BOTH paths (safe and raw EXECUTE). Raw + execute skips *semantic* checks, never resource limits: a runaway traversal + is just as expensive when a trusted app submits it. The database-side + backstop is a ``statement_timeout`` the emitter sets per transaction. +- **Semantic rules** — safe path only. Writes and procedure CALLs are + rejected here before translation; the true guard is the read-only + transaction the node runs safe queries in (server-side, like FalkorDB's + RO_QUERY), with this check giving precise, pre-flight errors. + +Caps are constructor arguments; the node currently wires only +``statement_timeout_ms`` through its config (``query_timeout_ms`` in +services.json) and leaves the rest at these defaults, which were +sanity-checked against the pinned AGE 1.5.0 container. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from .analysis import CypherFacts +from .errors import AgeFirewallRejected + +# Defaults tuned against the pinned container (see the layer README): +# unbounded/deep variable-length traversals are the main resource hazard — +# AGE expands them recursively; depth 10 on a connected graph is already huge. +DEFAULT_MAX_QUERY_LENGTH = 10_000 +DEFAULT_MAX_VAR_LENGTH_DEPTH = 10 +DEFAULT_STATEMENT_TIMEOUT_MS = 30_000 +# The ANTLR parser burns ~14 stack frames per expression nesting level, so +# Python's default recursion limit trips around 70 nested parens. 50 keeps a +# deterministic, well-messaged rejection comfortably below that cliff. +DEFAULT_MAX_NESTING_DEPTH = 50 + + +@dataclass(frozen=True) +class FirewallConfig: + max_query_length: int = DEFAULT_MAX_QUERY_LENGTH + max_var_length_depth: int = DEFAULT_MAX_VAR_LENGTH_DEPTH + max_nesting_depth: int = DEFAULT_MAX_NESTING_DEPTH + # Applied by the emitter as SET LOCAL statement_timeout in the query's + # transaction — the database-side resource backstop for both paths. + statement_timeout_ms: int = DEFAULT_STATEMENT_TIMEOUT_MS + + +def check_pre_parse(cypher: str, config: FirewallConfig) -> None: + """Caps that must run BEFORE the parse — both paths. + + The ANTLR parse is the expensive step these caps exist to bound (linear + CPU in query length; stack depth in expression nesting), so checking them + after ``analyze()`` would defeat their purpose. Character scans only — + no parser involvement. Raises AgeFirewallRejected on violation. + + The nesting scan counts brackets inside string literals too; a legitimate + string containing 50+ consecutive open brackets is contrived enough that + the deterministic pre-parse rejection is the better trade. + """ + if len(cypher) > config.max_query_length: + raise AgeFirewallRejected( + 'max_query_length', + f'query is {len(cypher)} chars (limit {config.max_query_length})', + ) + depth = 0 + for ch in cypher: + if ch in '([{': + depth += 1 + if depth > config.max_nesting_depth: + raise AgeFirewallRejected( + 'max_nesting_depth', + f'expression nesting exceeds {config.max_nesting_depth} levels ' + '(reduce parenthesis/bracket nesting)', + ) + elif ch in ')]}': + depth = max(0, depth - 1) + + +def check_resource_caps(facts: CypherFacts, config: FirewallConfig) -> None: + """Resource caps — both paths. Raises AgeFirewallRejected on violation.""" + if len(facts.query) > config.max_query_length: + raise AgeFirewallRejected( + 'max_query_length', + f'query is {len(facts.query)} chars (limit {config.max_query_length})', + ) + + for lower, upper in facts.var_length_ranges: + if upper is None: + raise AgeFirewallRejected( + 'unbounded_var_length', + f'variable-length pattern without an upper bound (use e.g. *1..{config.max_var_length_depth})', + ) + if upper > config.max_var_length_depth: + raise AgeFirewallRejected( + 'max_var_length_depth', + f'variable-length upper bound {upper} exceeds limit {config.max_var_length_depth}', + ) + if lower is not None and lower > upper: + raise AgeFirewallRejected('invalid_var_length_range', f'lower bound {lower} exceeds upper bound {upper}') + + +def check_semantics_readonly(facts: CypherFacts) -> None: + """Semantic rules — safe path only. Rejects writes and procedure CALLs.""" + if facts.write_clauses: + clauses = ', '.join(sorted(facts.write_clauses)) + raise AgeFirewallRejected( + 'write_clause', + f'{clauses} not allowed on the read-only path (use the execute path for writes)', + ) + if facts.has_call: + raise AgeFirewallRejected('procedure_call', 'CALL is not allowed on the read-only path') diff --git a/packages/ai/src/ai/common/graph/age/translate.py b/packages/ai/src/ai/common/graph/age/translate.py new file mode 100644 index 000000000..58116de09 --- /dev/null +++ b/packages/ai/src/ai/common/graph/age/translate.py @@ -0,0 +1,107 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""The translation pipeline: Cypher text in, executable SQL plan out. + +Stage order (per the design): + +1. **Parse** — openCypher ANTLR grammar (:mod:`.analysis`). +2. **Firewall** — resource caps on both paths; semantic read-only rules on + the safe path only (:mod:`.firewall`). +3. **Dialect** — capability table keyed by AGE version (:mod:`.capabilities`). +4. **Emit** — ``cypher()`` envelope, synthesized column list, bind plan + (:mod:`.emit`). + +Decoding results back to Python is :func:`~.decode.decode_agtype`; row +assembly from the emitted plan is :func:`decode_row` here. The layer is a +pure transform — no database access — so every stage unit-tests offline. +""" + +from __future__ import annotations + +import enum +from typing import Any, Dict, List, Optional, Sequence + +from .analysis import analyze +from .capabilities import DEFAULT_AGE_VERSION, apply_capabilities +from .decode import decode_agtype +from .emit import TranslatedQuery, emit +from .firewall import FirewallConfig, check_pre_parse, check_resource_caps, check_semantics_readonly + + +class TranslateMode(enum.Enum): + """Which firewall posture the caller runs under.""" + + # The LLM/tool path: semantic read-only rules + resource caps; the node + # runs the plan in a READ ONLY transaction. + SAFE = 'safe' + # The gated EXECUTE path: resource caps still apply, semantics don't. + RAW = 'raw' + + +def translate( + cypher: str, + params: Optional[Dict[str, Any]] = None, + limit: Optional[int] = None, + mode: TranslateMode = TranslateMode.SAFE, + graph_name: str = 'rocketride', + age_version: str = DEFAULT_AGE_VERSION, + firewall: Optional[FirewallConfig] = None, +) -> TranslatedQuery: + """Translate one Cypher query into an executable AGE SQL plan. + + Raises: + AgeTranslationError: the text does not parse (or cannot be emitted). + AgeUnsupportedFeature: a capability cell rejects the construct. + AgeFirewallRejected: a resource cap or (safe path) semantic rule fires. + """ + config = firewall or FirewallConfig() + + # Length/nesting caps run BEFORE the parse — the parse is the expensive + # step they bound, so an oversize query is rejected in O(n) scan time + # instead of after seconds of ANTLR work. + check_pre_parse(cypher, config) + + facts = analyze(cypher) + + # Firewall: resource caps guard BOTH paths; only semantics are mode-gated. + check_resource_caps(facts, config) + if mode is TranslateMode.SAFE: + check_semantics_readonly(facts) + + facts = apply_capabilities(facts, age_version) + + return emit( + facts, + graph_name=graph_name, + params=params, + limit=limit, + statement_timeout_ms=config.statement_timeout_ms, + read_only=mode is TranslateMode.SAFE, + ) + + +def decode_row(plan: TranslatedQuery, raw_row: Sequence[Any]) -> Dict[str, Any]: + """Decode one raw result row into a dict keyed by Cypher display names.""" + names: List[str] = plan.columns or [] + return {(names[i] if i < len(names) else f'col{i}'): decode_agtype(value) for i, value in enumerate(raw_row)} diff --git a/packages/ai/src/ai/common/requirements.txt b/packages/ai/src/ai/common/requirements.txt index 0b30d2b15..ed4679617 100644 --- a/packages/ai/src/ai/common/requirements.txt +++ b/packages/ai/src/ai/common/requirements.txt @@ -6,3 +6,7 @@ json5 RestrictedPython SQLAlchemy>=2.0,<2.1 yappi +# graph/age translation layer: runtime for the vendored ANTLR-generated +# openCypher + agtype parsers (range, not ==: depends() compiles all +# requirement files into one constraint set) +antlr4-python3-runtime>=4.13.2,<4.14 diff --git a/packages/ai/src/ai/common/rocketride_db.py b/packages/ai/src/ai/common/rocketride_db.py new file mode 100644 index 000000000..7ee80c8eb --- /dev/null +++ b/packages/ai/src/ai/common/rocketride_db.py @@ -0,0 +1,194 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +""" +Shared connection resolution for the RocketRide cloud database nodes. + +``rocketride_sql``, ``rocketride_vector`` and ``rocketride_graph`` all connect +to the same per-tenant cloud database and take **no** connection config. Rather +than reading host/user/password, they resolve a ready DSN from the account layer +keyed by the authenticated ``client_id``. Centralising that resolution here +gives the three nodes one identical seam — and one place for tests to inject a +fake resolver — so node code is byte-for-byte independent of which +``Account.resolve_db_dsn`` implementation (OSS stub, real SaaS, or fake) is live. + +In the hosted path the task engine resolves the DSN server-side and injects it +as ``ROCKETRIDE_DB_DSN``; nodes never resolve identity themselves (identity +rides the task file since #1686 — the task engine does NOT set +``ROCKETRIDE_CLIENT_ID``). The env-based ``current_client_id`` accessor +remains for the account fallback used by server-process callers and tests. +The account call is async while node lifecycle code (``beginGlobal`` / +``Store.__init__``) is synchronous, so it is bridged with ``asyncio.run`` — +safe only from a thread with no running loop, which is exactly how nodes are +constructed. +""" + +import asyncio +import os +import urllib.parse + +# Identity override consumed only by the account fallback (server-process +# callers and tests). The task engine does NOT set this in node subprocesses: +# identity rides the task file (#1686) and the ROCKETRIDE_* env namespace is +# caller-influenced by design. +CLIENT_ID_ENV = 'ROCKETRIDE_CLIENT_ID' + +# The task engine resolves the per-tenant DSN server-side at task start (only +# the server process has SaaS account context) and injects it here for +# pipelines that contain RocketRide cloud DB nodes. +DB_DSN_ENV = 'ROCKETRIDE_DB_DSN' + +# When server-side resolution fails (e.g. a broker outage), the task engine +# passes the reason down here instead of a DSN, so the node can report the +# real cause rather than the misleading "sign into RocketRide cloud" error. +DB_RESOLVE_ERROR_ENV = 'ROCKETRIDE_DB_RESOLVE_ERROR' + + +def _run_async(coro): + """Run a coroutine from synchronous node lifecycle code. + + Only safe to call from a thread with no running event loop — node + ``beginGlobal`` / ``Store.__init__`` are invoked synchronously by the + engine, which is the supported caller. Pre-check so a misuse surfaces with + a clear message instead of ``asyncio.run``'s generic RuntimeError. + """ + try: + asyncio.get_running_loop() + except RuntimeError: + pass + else: + raise RuntimeError( + 'resolve_rocketride_dsn must not be called from a thread with a running event loop; ' + 'RocketRide DB node lifecycle methods are constructed synchronously by the engine.' + ) + return asyncio.run(coro) + + +def current_client_id() -> str: + """Return the authenticated ``client_id`` for the running task. + + Raises: + ValueError: if the identity env var is missing — which normally means + the node is running outside the task engine, or the connection is + not signed into RocketRide cloud. + """ + client_id = os.environ.get(CLIENT_ID_ENV, '').strip() + if not client_id: + raise ValueError( + f'{CLIENT_ID_ENV} is not set; RocketRide cloud DB nodes require a signed-in RocketRide cloud identity' + ) + return client_id + + +def to_sqlalchemy_url(dsn: str) -> str: + """Normalise a libpq URL DSN to a SQLAlchemy psycopg2 URL. + + ``psycopg2.connect`` accepts a bare ``postgresql://`` / ``postgres://`` URL, + but SQLAlchemy's ``create_engine`` needs an explicit driver in the scheme. + A DSN that already names a driver (``postgresql+psycopg2://``) or is a + key/value libpq string (``host=... dbname=...``) is passed through + unchanged. + """ + if '+' in dsn.split(':', 1)[0]: + # Scheme already carries a driver (e.g. postgresql+psycopg2://). + return dsn + for prefix in ('postgresql://', 'postgres://'): + if dsn.startswith(prefix): + return 'postgresql+psycopg2://' + dsn[len(prefix) :] + return dsn + + +def resolve_rocketride_dsn() -> str: + """Resolve the per-tenant libpq DSN for the current client. + + Delivery order: + + 1. ``ROCKETRIDE_DB_DSN`` env — the production path. The task engine + resolves the DSN server-side at task start (the SaaS account context + exists only in the server process; node subprocesses always see the + OSS account) and injects it here at task start. + 2. ``Account.resolve_db_dsn(client_id)`` — fallback for callers running + inside the server process (or tests injecting a fake account). On an + unconfigured open-source build this raises the cloud-sign-in error. + + Returns the raw DSN (URL form, directly usable by ``psycopg2.connect``). + Callers that need a SQLAlchemy engine URL should wrap the result in + :func:`to_sqlalchemy_url`. + """ + injected = os.environ.get(DB_DSN_ENV, '').strip() + if injected: + return injected + + # Server-side resolution was attempted and failed: report that failure, + # not the sign-in error the account fallback below would produce (the + # task engine scrubs the broker env from node subprocesses, so the + # fallback cannot succeed here anyway). + resolve_error = os.environ.get(DB_RESOLVE_ERROR_ENV, '').strip() + if resolve_error: + raise ValueError(f'RocketRide cloud database resolution failed at task start: {resolve_error}') + + # Lazy import: ai.common is imported very early, and ai.account pulls in the + # OSS/SaaS overlay — importing at module load risks a cycle. + from ai.account import account + + dsn = _run_async(account.resolve_db_dsn(current_client_id())) + if not dsn or not isinstance(dsn, str): + raise ValueError('Account.resolve_db_dsn returned an empty DSN') + return dsn + + +def parse_dsn_fields(dsn: str) -> dict: + """Best-effort parse of a URL-form DSN into host/user/password/database. + + Used only to populate cosmetic / bookkeeping fields (e.g. the base class's + ``database`` used in log messages and the vector node's connection subKey). + The DSN itself remains the source of truth for the actual connection. + Non-URL DSNs yield empty fields. + """ + empty = {'host': '', 'port': None, 'user': '', 'password': '', 'database': ''} + try: + parsed = urllib.parse.urlparse(dsn) + except ValueError: + # e.g. mismatched IPv6 brackets — urlparse itself rejects the URL. + return empty + if not parsed.scheme: + return empty + # hostname/port validate lazily and raise ValueError on malformed + # authorities (bad bracket host, non-numeric or out-of-range port); these + # fields are cosmetic, so degrade per-field rather than fail node + # construction. + try: + host = parsed.hostname or '' + except ValueError: + host = '' + try: + port = parsed.port + except ValueError: + port = None + return { + 'host': host, + 'port': port, + 'user': urllib.parse.unquote(parsed.username) if parsed.username else '', + 'password': urllib.parse.unquote(parsed.password) if parsed.password else '', + 'database': (parsed.path or '').lstrip('/'), + } diff --git a/packages/ai/src/ai/modules/task/task_engine.py b/packages/ai/src/ai/modules/task/task_engine.py index 85384ddfd..70d570aec 100644 --- a/packages/ai/src/ai/modules/task/task_engine.py +++ b/packages/ai/src/ai/modules/task/task_engine.py @@ -380,6 +380,65 @@ def _resolve_pipeline(self, pipeline: Dict[str, Any]) -> Dict[str, Any]: """ return resolve_pipeline_env(pipeline, self._env) + # Providers that connect to the per-tenant RocketRide cloud database and + # therefore need ROCKETRIDE_DB_DSN injected into the task subprocess. + _ROCKETRIDE_DB_PROVIDERS = frozenset({'rocketride_sql', 'rocketride_vector', 'rocketride_graph'}) + + def _pipeline_uses_rocketride_db(self) -> bool: + """True when any pipeline component is a RocketRide cloud DB node.""" + components = self._pipeline.get('components') or [] + return any( + isinstance(component, dict) and component.get('provider') in self._ROCKETRIDE_DB_PROVIDERS + for component in components + ) + + async def _build_subprocess_env(self) -> Dict[str, str]: + """Build the environment for the task subprocess. + + Credential hygiene for the RocketRide cloud DB path: + + - The broker credential can resolve ANY tenant's DSN — it must never + reach node subprocesses, which run user pipeline code. + - A DSN inherited from the parent environment must not leak into + pipelines that didn't resolve one (and must not survive a broker + failure as a stale value pointing at who-knows-which tenant). + + Children only ever get the single DSN resolved here, for pipelines + that actually contain one of the DB nodes. Identity is NOT delivered + through the environment (it rides the task file's 'identity' block — + the ROCKETRIDE_* env namespace is caller-influenced by design). + """ + subprocess_env = os.environ.copy() + + subprocess_env.pop('ROCKETRIDE_DB_BROKER_URL', None) + subprocess_env.pop('ROCKETRIDE_DB_BROKER_TOKEN', None) + subprocess_env.pop('ROCKETRIDE_DB_DSN', None) + subprocess_env.pop('ROCKETRIDE_DB_RESOLVE_ERROR', None) + + # Resolve the per-tenant DSN server-side (the SaaS account context + # exists only in this process) and hand it to the node subprocess via + # env — the same delivery mechanism as ROCKETRIDE_CLIENT_ID. Scoped to + # pipelines that actually contain one of the DB nodes so unrelated + # tasks never trigger provisioning. Resolution failure is non-fatal + # here: the node surfaces its own clear error, with the failure reason + # passed down so it isn't misreported as a sign-in problem. + if self._pipeline_uses_rocketride_db(): + try: + from ai.account import account + + dsn = await account.resolve_db_dsn(self.client_id) + subprocess_env['ROCKETRIDE_DB_DSN'] = dsn + except NotImplementedError: + # Broker env not configured (open-source default) — the + # node raises the sign-in message itself. + pass + except Exception as e: + self.debug_message(f'RocketRide DB DSN resolution failed: {e}') + reason = (str(e).strip().splitlines() or [repr(e)])[0] + subprocess_env['ROCKETRIDE_DB_RESOLVE_ERROR'] = reason + + return subprocess_env + def _check_pipeline(self, pipeline: Dict[str, Any]) -> None: """ Validate pipeline configuration and extract metadata. @@ -1979,7 +2038,9 @@ async def start_task(self) -> None: # Launch subprocess. Identity travels in the TASK FILE (see # _build_task's 'identity' block), never the environment — the # ROCKETRIDE_* env namespace is caller-influenced by design. - subprocess_env = os.environ.copy() + # _build_subprocess_env additionally scrubs the RocketRide DB + # broker credentials and injects the resolved per-tenant DSN. + subprocess_env = await self._build_subprocess_env() # avoidMocks: strip ROCKETRIDE_MOCK so node.py loads real libraries if self._pipeline.get('avoidMocks'): diff --git a/packages/ai/tests/ai/account/test_resolve_db_dsn.py b/packages/ai/tests/ai/account/test_resolve_db_dsn.py new file mode 100644 index 000000000..17106014d --- /dev/null +++ b/packages/ai/tests/ai/account/test_resolve_db_dsn.py @@ -0,0 +1,205 @@ +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Tests for ``AccountBase.resolve_db_dsn`` — the env-gated DB broker call. + +The broker contract (locked 2026-07-23, shipped as saas #381): +``POST {ROCKETRIDE_DB_BROKER_URL} {"tenant_id": ...}`` with a Bearer token -> +``{"database", "role", "dsn", "created"}`` (only ``dsn`` is read), idempotent. +A real localhost HTTP server plays the broker so the actual urllib code path +is exercised. +""" + +from __future__ import annotations + +import asyncio +import json +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer + +import pytest + +from ai.account.base import AccountBase + +TEST_DSN = 'postgresql://r_t1_ab:secret@pooler.internal:5432/t_t1_ab?sslmode=require' +TEST_TOKEN = 'broker-token-123' + + +class _Minimal(AccountBase): + async def authenticate(self, credential): # pragma: no cover - unused + return None + + +class _FakeBroker(BaseHTTPRequestHandler): + """Stands in for the data-core provisioner.""" + + requests: list = [] + + def do_POST(self): # noqa: N802 (http.server API) + length = int(self.headers.get('Content-Length', 0)) + body = json.loads(self.rfile.read(length) or b'{}') + type(self).requests.append({'auth': self.headers.get('Authorization'), 'body': body}) + + tenant = body.get('tenant_id', '') + if self.headers.get('Authorization') != f'Bearer {TEST_TOKEN}': + self.send_response(401) + self.end_headers() + return + if tenant == 'unknown-tenant': + self.send_response(404) + self.end_headers() + return + if tenant == 'no-dsn-tenant': + payload = {'database': 't_broken', 'role': 't_broken_rw', 'created': False} + elif tenant == 'no-sslmode-tenant': + bare = 'postgresql://r_t1_ab:secret@pooler.internal:5432/t_t1_ab' + payload = {'database': 't_t1_ab', 'role': 't_t1_ab_rw', 'dsn': bare, 'created': False} + elif tenant == 'bad-scheme-tenant': + payload = {'database': 't_t1_ab', 'role': 't_t1_ab_rw', 'dsn': 'mysql://u:p@h/db', 'created': False} + else: + payload = {'database': 't_t1_ab', 'role': 't_t1_ab_rw', 'dsn': TEST_DSN, 'created': False} + raw = json.dumps(payload).encode('utf-8') + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(raw))) + self.end_headers() + self.wfile.write(raw) + + def log_message(self, *args): # silence request logging + pass + + +@pytest.fixture() +def broker(monkeypatch): + _FakeBroker.requests = [] + server = HTTPServer(('127.0.0.1', 0), _FakeBroker) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + url = f'http://127.0.0.1:{server.server_port}/provision' + monkeypatch.setenv('ROCKETRIDE_DB_BROKER_URL', url) + monkeypatch.setenv('ROCKETRIDE_DB_BROKER_TOKEN', TEST_TOKEN) + yield _FakeBroker + server.shutdown() + server.server_close() + + +def _resolve(client_id: str) -> str: + return asyncio.run(_Minimal().resolve_db_dsn(client_id)) + + +def test_unconfigured_env_raises_signin_error(monkeypatch): + monkeypatch.delenv('ROCKETRIDE_DB_BROKER_URL', raising=False) + monkeypatch.delenv('ROCKETRIDE_DB_BROKER_TOKEN', raising=False) + with pytest.raises(NotImplementedError, match='require signing into RocketRide cloud'): + _resolve('tenant-1') + + +def test_partial_env_still_raises(monkeypatch, broker): + monkeypatch.delenv('ROCKETRIDE_DB_BROKER_TOKEN', raising=False) + with pytest.raises(NotImplementedError): + _resolve('tenant-1') + + +def test_resolves_dsn_via_broker(broker): + assert _resolve('tenant-1') == TEST_DSN + + +def test_client_id_passed_verbatim_as_tenant_id_with_bearer(broker): + _resolve('Client-42 ') + assert broker.requests == [{'auth': f'Bearer {TEST_TOKEN}', 'body': {'tenant_id': 'Client-42'}}] + + +def test_idempotent_recall_returns_same_dsn(broker): + assert _resolve('tenant-1') == _resolve('tenant-1') == TEST_DSN + assert len(broker.requests) == 2 + + +def test_broker_4xx_raises_runtime_error(broker): + with pytest.raises(RuntimeError, match='rejected provision.*404'): + _resolve('unknown-tenant') + + +def test_bad_token_raises_runtime_error(broker, monkeypatch): + monkeypatch.setenv('ROCKETRIDE_DB_BROKER_TOKEN', 'wrong') + with pytest.raises(RuntimeError, match='rejected provision.*401'): + _resolve('tenant-1') + + +def test_missing_dsn_in_response_raises(broker): + with pytest.raises(RuntimeError, match='did not include a DSN'): + _resolve('no-dsn-tenant') + + +def test_unreachable_broker_raises(monkeypatch): + monkeypatch.setenv('ROCKETRIDE_DB_BROKER_URL', 'http://127.0.0.1:9/provision') + monkeypatch.setenv('ROCKETRIDE_DB_BROKER_TOKEN', TEST_TOKEN) + with pytest.raises(RuntimeError, match='unreachable'): + _resolve('tenant-1') + + +def test_empty_client_id_rejected(broker): + with pytest.raises(ValueError, match='non-empty client_id'): + _resolve(' ') + + +# --------------------------------------------------------------------------- +# Transport security guards +# --------------------------------------------------------------------------- + + +def test_http_broker_url_rejected_for_non_localhost(monkeypatch): + """The broker token can resolve any tenant's DSN — never send it over + plain http to a remote host (a deployment typo must fail loudly). + """ + monkeypatch.setenv('ROCKETRIDE_DB_BROKER_URL', 'http://broker.internal/provision') + monkeypatch.setenv('ROCKETRIDE_DB_BROKER_TOKEN', TEST_TOKEN) + with pytest.raises(RuntimeError, match='must use https'): + _resolve('tenant-1') + + +def test_https_and_localhost_http_broker_urls_accepted(): + # https anywhere; plain http only on the loopback (local rig / this suite). + AccountBase._check_broker_url('https://api.rocketride.ai/db/provision') + AccountBase._check_broker_url('http://localhost:8090/provision') + AccountBase._check_broker_url('http://127.0.0.1:8090/provision') + with pytest.raises(RuntimeError, match='must use https'): + AccountBase._check_broker_url('ftp://broker.internal/provision') + with pytest.raises(RuntimeError, match='must use https'): + AccountBase._check_broker_url('file:///etc/passwd') + + +def test_dsn_without_sslmode_gets_require_pinned(broker): + """A broker regression that drops sslmode must not silently downgrade + every tenant connection to cleartext. + """ + dsn = _resolve('no-sslmode-tenant') + assert dsn == 'postgresql://r_t1_ab:secret@pooler.internal:5432/t_t1_ab?sslmode=require' + + +def test_dsn_with_existing_sslmode_passes_through_unchanged(broker): + assert _resolve('tenant-1') == TEST_DSN + + +def test_non_postgres_dsn_rejected(broker): + with pytest.raises(RuntimeError, match='non-PostgreSQL DSN'): + _resolve('bad-scheme-tenant') diff --git a/packages/ai/tests/ai/modules/task/test_task_engine.py b/packages/ai/tests/ai/modules/task/test_task_engine.py index ddea26881..ddfaf042c 100644 --- a/packages/ai/tests/ai/modules/task/test_task_engine.py +++ b/packages/ai/tests/ai/modules/task/test_task_engine.py @@ -736,6 +736,111 @@ async def test_forward_task_event_debugger_swallows_send_failure(): await Task._forward_task_event(t, EVENT_TYPE.DEBUGGER, {'event': 'output'}) +# --------------------------------------------------------------------------- +# _pipeline_uses_rocketride_db +# --------------------------------------------------------------------------- + + +def test_pipeline_uses_rocketride_db_detects_each_provider(): + """Any of the three RocketRide cloud DB providers triggers DSN injection.""" + for provider in ('rocketride_sql', 'rocketride_vector', 'rocketride_graph'): + t = _task(pipeline={'components': [{'id': 'a', 'provider': 'chat'}, {'id': 'b', 'provider': provider}]}) + assert Task._pipeline_uses_rocketride_db(t), provider + + +def test_pipeline_uses_rocketride_db_false_without_db_nodes(): + """Ordinary pipelines never trigger provisioning.""" + t = _task(pipeline={'components': [{'id': 'a', 'provider': 'chat'}, {'id': 'b', 'provider': 'db_postgres'}]}) + assert not Task._pipeline_uses_rocketride_db(t) + + +def test_pipeline_uses_rocketride_db_tolerates_malformed_components(): + """Missing components / non-dict entries must not raise at task start.""" + assert not Task._pipeline_uses_rocketride_db(_task(pipeline={})) + t = _task(pipeline={'components': ['not-a-dict', {'no-provider': True}]}) + assert not Task._pipeline_uses_rocketride_db(t) + + +# --------------------------------------------------------------------------- +# _build_subprocess_env — RocketRide DB credential hygiene +# --------------------------------------------------------------------------- + +_DB_PIPELINE = {'components': [{'id': 'db', 'provider': 'rocketride_sql'}]} + + +def _env_task(pipeline=None): + t = _task(pipeline=pipeline if pipeline is not None else {}) + t.client_id = 'client-env-test' + return t + + +def _patch_resolve(monkeypatch, fake): + import ai.account + + monkeypatch.setattr(ai.account.account, 'resolve_db_dsn', fake) + + +@pytest.mark.asyncio +async def test_subprocess_env_scrubs_broker_credentials(monkeypatch): + """The broker credential can mint ANY tenant's DSN — it must never reach + node subprocesses, and neither may a parent-level DSN or stale error. + """ + monkeypatch.setenv('ROCKETRIDE_DB_BROKER_URL', 'https://broker.example') + monkeypatch.setenv('ROCKETRIDE_DB_BROKER_TOKEN', 'super-secret') + monkeypatch.setenv('ROCKETRIDE_DB_DSN', 'postgresql://stale@parent/db') + monkeypatch.setenv('ROCKETRIDE_DB_RESOLVE_ERROR', 'stale reason') + + env = await Task._build_subprocess_env(_env_task()) # no DB nodes + + assert 'ROCKETRIDE_DB_BROKER_URL' not in env + assert 'ROCKETRIDE_DB_BROKER_TOKEN' not in env + assert 'ROCKETRIDE_DB_DSN' not in env + assert 'ROCKETRIDE_DB_RESOLVE_ERROR' not in env + # Identity rides the task file (#1686), never the environment. + assert 'ROCKETRIDE_CLIENT_ID' not in env + + +@pytest.mark.asyncio +async def test_subprocess_env_injects_resolved_dsn(monkeypatch): + async def fake_resolve(client_id): + assert client_id == 'client-env-test' + return 'postgresql://tenant@pooler/db?sslmode=require' + + _patch_resolve(monkeypatch, fake_resolve) + env = await Task._build_subprocess_env(_env_task(pipeline=_DB_PIPELINE)) + assert env['ROCKETRIDE_DB_DSN'] == 'postgresql://tenant@pooler/db?sslmode=require' + + +@pytest.mark.asyncio +async def test_subprocess_env_stale_dsn_does_not_survive_broker_failure(monkeypatch): + """A parent-env DSN must not become the node's DSN when resolution fails — + it could point at another tenant. The failure reason is passed down instead. + """ + monkeypatch.setenv('ROCKETRIDE_DB_DSN', 'postgresql://stale@parent/other-tenant') + + async def fake_resolve(client_id): + raise RuntimeError('DB broker request failed: HTTP 503') + + _patch_resolve(monkeypatch, fake_resolve) + t = _env_task(pipeline=_DB_PIPELINE) + env = await Task._build_subprocess_env(t) + + assert 'ROCKETRIDE_DB_DSN' not in env + assert env['ROCKETRIDE_DB_RESOLVE_ERROR'] == 'DB broker request failed: HTTP 503' + t.debug_message.assert_called_once() + + +@pytest.mark.asyncio +async def test_subprocess_env_unconfigured_account_is_nonfatal(monkeypatch): + async def fake_resolve(client_id): + raise NotImplementedError('sign in') + + _patch_resolve(monkeypatch, fake_resolve) + env = await Task._build_subprocess_env(_env_task(pipeline=_DB_PIPELINE)) + assert 'ROCKETRIDE_DB_DSN' not in env + assert 'ROCKETRIDE_DB_RESOLVE_ERROR' not in env + + # --------------------------------------------------------------------------- # _accumulate_analytics — run analytics in the status body # --------------------------------------------------------------------------- diff --git a/packages/docs/content-static/integrations/neo4j.md b/packages/docs/content-static/integrations/neo4j.md index ac0ba9ff2..cabde47e8 100644 --- a/packages/docs/content-static/integrations/neo4j.md +++ b/packages/docs/content-static/integrations/neo4j.md @@ -5,7 +5,7 @@ sidebar_position: 4 # Neo4j -The `db_neo4j` node answers natural-language questions against a Neo4j graph +The `graph_neo4j` node answers natural-language questions against a Neo4j graph database by translating them to Cypher with a connected LLM. It plays two roles: a **pipeline node** that takes questions on the `questions` lane and emits results downstream, and an **agent tool** that exposes graph retrieval directly @@ -51,7 +51,7 @@ The node connects over the Bolt protocol with the official neo4j Python driver. ```json { "id": "graph_1", - "provider": "neo4jdb", + "provider": "graph_neo4j", "config": { "uri": "neo4j+s://your-instance.databases.neo4j.io", "auth_method": "userpass", @@ -101,7 +101,8 @@ prompt so Cypher is generated against the real structure. ## Agent tools When connected to an agent, the node exposes three functions under the node's -prefix (e.g. `neo4j.get_data`): +ID as a prefix (`.get_data` — e.g. `graph_1.get_data` for the sample +config above): | Tool | What it does | | --- | --- | @@ -122,6 +123,6 @@ fast rather than mid-pipeline. ## Related -- [`db_neo4j` node reference](/nodes/db_neo4j) +- [`graph_neo4j` node reference](/nodes/graph_neo4j) - [Qdrant integration](/integrations/qdrant) — vector retrieval for RAG - [Concepts: Agents & Tools](/concepts/agents-tools-skills) diff --git a/pyproject.toml b/pyproject.toml index d26bef27f..c3302e0f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,11 @@ line-length = 120 # Target Python version target-version = "py310" -# Directories to exclude from linting +# Directories to exclude from linting. +# force-exclude makes the list apply even when paths are passed explicitly +# (lefthook's `ruff check {staged_files}`), which is what keeps the vendored +# ANTLR-generated parsers out of lint. +force-exclude = true exclude = [ ".git", ".venv", @@ -24,6 +28,10 @@ exclude = [ "node_modules", ".eggs", "*.egg-info", + # Vendored third-party code (Apache-2.0) incl. ANTLR-generated parsers — + # not held to local style; provenance in ai/common/graph/age/README.md + "packages/ai/src/ai/common/graph/age/_agtype", + "packages/ai/src/ai/common/graph/age/_cypher", ] [tool.ruff.format]