Поддержка Cypher¶
Provisa переводит подмножество openCypher в SQL через модуль provisa/cypher/. (REQ-345, REQ-347) Запросы разбираются собственным рекурсивно-нисходящим парсером (без внешней библиотеки Cypher) (REQ-571), разрешаются по схеме относительно семантического слоя (REQ-351) и выдаются в виде SQL, затем маршрутизируются на целевой движок выполнения. (REQ-066, REQ-067, REQ-347)
Реализованные возможности¶
Предложения (Clauses)¶
| Предложение | Статус | Примечания |
|---|---|---|
MATCH (n:Label) |
✓ | Паттерны узлов с метками, переменными, встроенными свойствами |
OPTIONAL MATCH |
✓ | Выдаёт LEFT JOIN |
WHERE |
✓ | Полная поддержка выражений; применяется после MATCH |
RETURN |
✓ | Звёздочка, доступ к свойствам, выражения, алиасы |
RETURN DISTINCT |
✓ | Выдаёт SELECT DISTINCT |
WITH |
✓ | Выдаёт именованный CTE (_w0, _w1, …); поддерживает WITH … WHERE |
ORDER BY |
✓ | ASC / DESC |
SKIP / LIMIT |
✓ | Отображается на SQL OFFSET / LIMIT |
UNION / UNION ALL |
✓ | Рекурсивное объединение по под-AST |
CALL { … } |
✓ | Декомпозиция подзапроса верхнего уровня call через cypher_calls_to_sql_list |
CALL { WITH x … } |
✓ | Коррелированный подзапрос → CROSS JOIN LATERAL; см. §Коррелированный CALL |
CALL db.labels() |
✓ | Возвращает метки узлов из семантического слоя; без SQL-перевода (REQ-572) |
CALL db.relationshipTypes() |
✓ | Возвращает типы связей из семантического слоя (REQ-572) |
CALL db.propertyKeys() |
✓ | Возвращает все имена ключей свойств по всем типам узлов (REQ-572) |
UNWIND |
✓ | Развёртывание массива в строки; первый элемент становится FROM, последующие — CROSS JOIN UNNEST |
Паттерны сопоставления (Match Patterns)¶
| Паттерн | Статус | Примечания |
|---|---|---|
(n) — узел без метки |
✓ | UNION ALL по всем известным типам |
(n:Label) |
✓ | Отображается на зарегистрированную таблицу для этого типа GraphQL |
(n:Label {prop: val}) |
✓ | Встроенный фильтр свойства становится WHERE |
(a)-[:TYPE]->(b) |
✓ | Направленный, один переход |
(a)<-[:TYPE]-(b) |
✓ | Обратный обход; столбцы соединения меняются местами |
(a)-[]->(b) |
✓ | Любая направленная связь a→b; UNION ALL, если совпадает несколько типов |
(a)-[]-(b) |
✓ | Двунаправленная; расширяется до UNION ALL всех прямых и обратных связей |
(a)-[:TYPE*..N]->(b) |
✓ | Переменная длина с верхней границей; рекурсивный CTE для само-ссылающихся, иначе плоский JOIN |
(a)-[]->(b)-[]->(c) |
✓ | Многошаговые цепочки JOIN |
(n:DomainLabel) |
✓ | Доменная метка → подзапрос UNION ALL по всем типам домена |
(n:A\|B) |
✓ | Альтернация меток → специальный домен, добавленный в карту меток; UNION ALL по совпадающим типам |
shortestPath(…) |
✓ | Плоский JOIN для разнородных конечных точек; WITH RECURSIVE CTE для одного типа/само-ссылающихся |
allShortestPaths(…) |
✓ | То же, что shortestPath, но без LIMIT 1 |
Выражения и предикаты¶
| Возможность | Статус | Отображение в SQL |
|---|---|---|
Доступ к свойству n.prop |
✓ | n."prop" |
Параметры $name |
✓ | Позиционный $N |
Устаревшие параметры {name} |
✓ | Нормализуются в $name на этапе разбора |
Сравнение =, <>, <, >, <=, >= |
✓ | Прямое |
AND, OR, NOT |
✓ | Прямое |
IS NULL / IS NOT NULL |
✓ | Прямое |
IN [список] |
✓ | SQL IN; синтаксис скобок Cypher [...] переписывается в (...) |
STARTS WITH |
✓ | starts_with(col, val) |
ENDS WITH |
✓ | col LIKE CONCAT('%', val) |
CONTAINS |
✓ | strpos(col, val) > 0 |
=~ regex |
✓ | regexp_like(col, pattern) |
exists(n.prop) |
✓ | (n.prop) IS NOT NULL |
EXISTS { MATCH … } |
✓ | Коррелированный подзапрос EXISTS (SELECT 1 FROM …) |
COUNT { MATCH … } |
✓ | Коррелированный подзапрос (SELECT count(*) FROM …) |
COLLECT { MATCH … RETURN x } |
✓ | Коррелированный подзапрос ARRAY(SELECT x FROM …) |
id(n) |
✓ | Разрешается в настроенный ID-столбец узла |
labels(n) |
✓ | ARRAY['Label'] |
keys(n) |
✓ | ARRAY['prop1', 'prop2', …] |
type(r) |
✓ | Разрешается на этапе компиляции в строковый литерал 'REL_TYPE'; без столбца во время выполнения |
length(p) |
✓ | _t.hops для путей рекурсивного CTE; 1 для путей плоского JOIN |
CASE WHEN … THEN … ELSE … END |
✓ | Прямое (искомая и простая формы) |
| Неявный GROUP BY | ✓ | Неагрегированные элементы RETURN становятся ключами GROUP BY, когда любой элемент содержит агрегат |
Проекции карт (Map Projections)¶
| Синтаксис | Отображение в SQL |
|---|---|
n { .prop1, .prop2 } |
MAP(ARRAY['prop1','prop2'], ARRAY[n."prop1",n."prop2"]) |
n { .* } |
MAP(ARRAY[all props...], ARRAY[n."col",...]) — раскрывается из схемы |
n { .*, extra: expr } |
Все свойства схемы плюс именованный ключ; объединённый MAP |
n { key: expr } |
MAP(ARRAY['key'], ARRAY[expr]) |
Функции агрегации¶
| Cypher | SQL |
|---|---|
count(*), count(x) |
прямое |
count(DISTINCT x) |
count(DISTINCT x) |
collect(x) |
array_agg(x) |
avg, sum, min, max |
прямое |
stDev(x) |
stddev_samp(x) |
stDevP(x) |
stddev_pop(x) |
percentileCont(x, p) |
approx_percentile(x, p) |
percentileDisc(x, p) |
approx_percentile(x, p) |
Строковые функции¶
| Cypher | SQL |
|---|---|
toLower(x) |
lower(x) |
toUpper(x) |
upper(x) |
ltrim(x), rtrim(x), trim(x) |
прямое |
replace(x, a, b) |
прямое |
reverse(x) |
прямое |
split(x, d) |
прямое |
left(x, n) |
left(x, n) |
right(x, n) |
right(x, n) |
substring(x, start, len) |
substr(x, start+1, len) (индексация 0→1) |
size(string) |
char_length(string) |
size(list) |
cardinality(list) |
Функции преобразования типов¶
| Cypher | SQL |
|---|---|
toString(x) |
CAST(x AS VARCHAR) |
toInteger(x) |
TRY_CAST(x AS BIGINT) |
toFloat(x) |
TRY_CAST(x AS DOUBLE) |
toBoolean(x) |
TRY_CAST(x AS BOOLEAN) |
toStringOrNull, toIntegerOrNull, toFloatOrNull, toBooleanOrNull |
варианты TRY_CAST |
Математические функции¶
| Cypher | SQL |
|---|---|
log(x) |
ln(x) (натуральный логарифм) |
log2(x) |
log2(x) |
range(start, end) |
sequence(start, end) |
abs, sqrt, ceil, floor, round, sign |
передаются без изменений |
Функции списков¶
| Cypher | SQL |
|---|---|
head(list) |
element_at(list, 1) |
last(list) |
element_at(list, -1) |
tail(list) |
slice(list, 2, cardinality(list)) |
isEmpty(list) |
cardinality(list) = 0 |
Списковые включения (List Comprehensions)¶
| Синтаксис | Отображение в SQL |
|---|---|
[x IN list \| f(x)] |
transform(list, x -> f(x)) |
[x IN list WHERE p(x)] |
filter(list, x -> p(x)) |
[x IN list WHERE p(x) \| f(x)] |
transform(filter(list, x -> p(x)), x -> f(x)) |
any(x IN list WHERE p(x)) |
any_match(list, x -> p(x)) |
all(x IN list WHERE p(x)) |
all_match(list, x -> p(x)) |
none(x IN list WHERE p(x)) |
none_match(list, x -> p(x)) |
single(x IN list WHERE p(x)) |
cardinality(filter(list, x -> p(x))) = 1 |
reduce(acc = init, x IN list \| expr) |
reduce(list, init, (acc, x) -> expr, acc -> acc) |
Включения по паттернам (Pattern Comprehensions)¶
| Синтаксис | Отображение в SQL |
|---|---|
[(a)-[:R]->(b) \| b.prop] |
ARRAY(SELECT b."prop" FROM ... WHERE a.fk = b.pk) |
[(a)-[]->(b:Label) \| b.prop] |
тип выводится из семантического слоя; та же форма подзапроса ARRAY |
Коррелированные подзапросы CALL¶
CALL { WITH x MATCH (x)-[:R]->(n) RETURN n.prop AS alias } переводится в CROSS JOIN LATERAL (SELECT n."prop" AS alias FROM ... WHERE x."pk" = n."fk"). (REQ-573) Правила:
- Переменная внешней области видимости (
x) должна присутствовать вWITH - Поддерживается несколько импортированных переменных (
WITH a, b) - Первая связь во внутреннем MATCH, чей источник — переменная, связанная через lateral, определяет внутренний
FROMи условие соединения - Некоррелированные блоки
CALL { ... }верхнего уровня (безWITH) обрабатываются черезcypher_calls_to_sql_list
Записи (Writes)¶
Cypher поддерживает три паттерна записи через эндпоинт /data/cypher, выполняемые provisa/cypher/write_translator.py. (REQ-818) [tool-verified: provisa/api/rest/cypher_router.py:415-545]
| Cypher | SQL | Req |
|---|---|---|
CREATE (n:Label {props}) |
INSERT INTO catalog.schema.table (cols) VALUES (vals) |
REQ-666 |
MATCH (n:Label) WHERE … DELETE n |
DELETE FROM catalog.schema.table WHERE … |
REQ-667 |
MATCH (n:Label) WHERE … SET n.prop = val, … |
UPDATE catalog.schema.table SET col = val, … WHERE … |
REQ-668 |
Имена свойств отображаются на столбцы через удаление доменного префикса и разрешение алиасов; скалярные значения Cypher приводятся к типу целевого столбца. (REQ-666, REQ-668) Тело ответа содержит счётчик affected_rows. (REQ-670)
Правила:
- Метка должна разрешаться ровно в одну зарегистрированную таблицу. Неоднозначные или неизвестные метки — это жёсткая ошибка; нечёткое сопоставление не производится. (REQ-661) Новые метки или типы нельзя создать через Cypher. (REQ-662)
- Каждая запись проверяется на ACL
writable_byцелевой таблицы; роль без прав на запись отклоняется на этапе компиляции. (REQ-663) - Базовый коннектор источника должен поддерживать DML. Источники только для чтения (Trino-федерированные, Iceberg без коннектора Delta) отклоняют записи на этапе перевода. (REQ-664)
- Связи нельзя записывать — они выводятся из соединений по внешнему ключу, а не хранятся как рёбра. Обращение к связи как к цели — жёсткая ошибка. (REQ-665)
- Записи проходят через полный конвейер записи: внедрение RLS и пост-мутационные хуки (инвалидация кеша ответов, пометка материализованного представления как устаревшего, события изменений Kafka, перезагрузка горячей таблицы). (REQ-798)
MERGE,DETACH DELETEиREMOVEне поддерживаются и отклоняются на этапе разбора. (REQ-671)
Доступ по протоколу¶
Cypher достигает того же управляемого конвейера через два транспорта:
- HTTP —
POST /data/cypherс телом JSON ({"query": "...", "params": {...}}). Возвращает типизированные строки илиaffected_rowsдля записей. Переменные графа в предложенииRETURNсериализуются как JSON: узлы несутid,label,tableLabelиproperties; рёбра несутidentity,start,end,type,properties,startNodeиendNode; пути несутnodes,edgesиlength/hops. (REQ-750) Зарегистрированные команды также вызываемы здесь черезCALL fn(args) YIELD col1, col2— позиционные аргументы отображаются на объявленные имена аргументов команды по порядку. (REQ-1156) [tool-verified:provisa/api/rest/registered_call.py:113-143] - Bolt — сервер бинарного протокола, совместимого с Neo4j (кодек PackStream, фреймирование с разбиением на части), который позволяет Neo4j Browser, Bloom и драйверам Bolt выполнять Cypher по федеративному графу. (REQ-802) Он запускается, когда
PROVISA_BOLT_PORTустановлен в ненулевое значение, и отключён по умолчанию; установитеPROVISA_BOLT_CERT/PROVISA_BOLT_KEYдля TLS. [tool-verified:provisa/api/app_startup.py:317-338] Аутентификация Bolt отображает принципала на пользователя, а базу данных — на роль:SHOW DATABASESперечисляет по одной записи на пару (представление × роль), названнуюprovisa_<role>(бизнес-домены) илиprovisa_ops_<role>(с доменами system/meta/ops);:useвыбирает активную роль и представление. (REQ-807) Связи получают устойчивые целочисленные ID через таблицуrel_ids, зеркалируя дизайнnode_ids. (REQ-806) Зарегистрированные команды вызываемы черезCALL command(args)— позиционные аргументы отображаются на объявленные имена аргументов по порядку; процедурыCALL dbms.*/CALL db.*имеют приоритет. (REQ-1156) [tool-verified:provisa/bolt/session.py:722-749]
Аналитика графов¶
POST /data/graph-analytics выполняет запрос Cypher, строит граф NetworkX в памяти из полученных узлов и рёбер, выполняет именованный алгоритм и вливает словарь _analytics в каждый узел и ребро перед возвратом их как JSON с полем elapsed_ms. (REQ-642) Ключи _analytics различаются по алгоритму: центральность даёт score; выявление сообществ даёт cluster; k-core даёт core_number; центральность по степени добавляет in_degree и out_degree. (REQ-643) Эндпоинт отклоняет графы выше настраиваемого размера (по умолчанию 10 000 узлов / 50 000 рёбер) с HTTP 413; Girvan-Newman ограничен 500 узлами, если вызывающая сторона не передаёт force=true. (REQ-650, REQ-651)
Ограничения¶
Проектные ограничения¶
-
Записи ограничены
CREATE,SETиDELETE. Они выполняются как прямые записи в таблицы через тот же конвейер, что и мутации GraphQL и SQL. (REQ-818, REQ-666, REQ-667, REQ-668) См. §Записи выше.MERGE,DETACH DELETEиREMOVEотклоняются на этапе разбора. (REQ-671, REQ-818) Процедуры APOC также отклоняются. -
Нет свойств у связей. Связи (
-[r:TYPE]->) существуют исключительно как метаданные соединения в семантическом слое. (REQ-574) Они не несут хранимых атрибутов, поэтомуWHERE r.since > 2020илиRETURN r.weightне имеют смысла и не поддерживаются. -
Двунаправленный обход
(a)-[]-(b)переписывается в UNION ALL прямого и обратного направлений всех совпадающих направленных связей из семантического слоя. (REQ-575) Каждая связь в семантическом слое направленная; двунаправленный синтаксис — это синтаксический сахар, расширяющийся в оба направления. Дополнительные ветви выдаются на самом внешнем уровне запроса — последующие паттерны MATCH в том же запросе не дублируются по ветвям (ограничение для многошагового MATCH с двунаправленностью). -
Рекурсивные пути требуют границы. Паттерны переменной длины (
[*]) должны включать верхнюю границу (например,[*..10]). (REQ-348) Неограниченный обход отклоняется на этапе разбора для предотвращения неконтролируемых рекурсивных CTE.
Замечания о поведении¶
-
shortestPathна непере-само-ссылающихся путях использует плоский JOIN, а не упорядочивание по hops. Когда начальный и конечный типы различаются и в схеме нет само-ссылающейся связи, транслятор выдаёт цепочку плоских JOIN (кратчайший путь схемы). (REQ-576) Он не выдаётORDER BY hops, потому что hops не отслеживаются в этом пути кода. Результат — структурно кратчайший путь схемы, а не кратчайший по данным путь среди нескольких строк. -
Несколько путей схемы производят
UNION ALL. Когда два пути схемы с одинаковым числом переходов соединяют одни и те же начальный и конечный типы (например,Person -[WORKS_AT]-> CompanyиPerson -[MANAGES]-> Company), оба выдаются как ветвиUNION ALL. (REQ-577) Дедупликация строк, появляющихся в обеих ветвях, не выполняется. -
Одна
RelationshipMappingна пару источник→цель и комбинацию rel_type. Если два поля GraphQL на одном исходном типе производят одну и ту же строкуrel_type(после приведения к верхнему регистру) для одного и того же целевого типа, вторая регистрация перезаписывает первую вCypherLabelMap.relationships. Ключ связи включает имена исходного и целевого типов, поэтому различные пары источник/цель с одинаковым именем типа получают собственные записи и не затрагиваются. -
CTE предложения
WITHназываются_w0,_w1, … (REQ-578) Имена назначаются позиционно в рамках одного вызова перевода. Композиция нескольких переведённых запросов (например, в пакете) может привести к коллизиям имён CTE, если они конкатенируются наивно.
Покрытие выражений и паттернов (REQ-913)¶
Выражения Cypher разбираются в AST и понижаются узел за узлом до SQL (provisa/cypher/expr_parser.py, provisa/cypher/expr_visitor.py). Грамматика следует иерархии приоритетов oC_Expression openCypher. Поддерживается: литералы, параметры, доступ к свойствам, n.prop, индексация и срезы, арифметика (+ - * / % ^), сравнение, IN, STARTS WITH / ENDS WITH / CONTAINS / =~, IS [NOT] NULL, булевы AND / OR / XOR / NOT, CASE, литералы списков и карт, включения списков и паттернов (включая привязку пути p = (…)), проекция карт, reduce, квантификаторы all / any / none / single, экзистенциальные подзапросы и вызовы функций.
-
Метки фиксированы; вы не можете создавать типы объектов через Cypher. Метка разрешается в известный домен, известный тип объекта или квалифицированный
domain:object_type— замкнутое множество, определённое зарегистрированной схемой. Cypher никогда не вводит новую метку или тип. Создание экземпляров возможно только для типов, уже определённых в записываемом источнике данных;CREATEзаписывает строки в такую таблицу (см. §Записи), но не может определить новую метку или тип. (REQ-662) Поддерживаются обе формы меток и означают один и тот же тест: постфикснаяn:Labelи развёрнутаяn IS :Label(и их отрицаниеn IS NOT :Label). Квалифицированная метка записывается какn:domain:object_type. -
shortestPathиallShortestPathsподдерживаются только внутриMATCH, а не как выражения. В паттерне (MATCH p = shortestPath((a:Person)-[:KNOWS*..5]->(b:Person))) они переводятся в CTEWITH RECURSIVEи требуют помеченных исходного и целевого узлов. При использовании в позиции выражения — например,RETURN shortestPath((a)-[*]->(b))илиWHERE length(shortestPath((a)-[*]->(b))) < 5— они не поддерживаются, потому что рекурсивное переписывание управляется предложениемMATCH, а не коррелированным подзапросом. -
Списковые включения,
REDUCEи квантификаторы работают со значениями списков; включения по паттернам обходят граф.reduce(...),all/any/none/single(...)и списковое включение[x IN list | …]работают над выражением списка и понижаются до функций списков высшего порядка движка — сами по себе они не обходят граф. Паттерновое включение[(a)-[:R]->(b) WHERE p | e]действительно обходит граф: его графовый паттерн адресуется как коррелированный подзапрос, поэтому это включение, источник которого — обход. Передавайте результаты обхода в списковые формы черезnodes(p)/relationships(p)/collect(...), либо используйте паттерновое включение напрямую.