Поддержка Cypher¶
Provisa переводит подмножество openCypher в SQL через модуль provisa/cypher/. Запросы разбираются собственным рекурсивно-нисходящим парсером (без внешней библиотеки Cypher), разрешаются по схеме относительно модели и выдаются в виде SQL, затем маршрутизируются на целевой движок выполнения.
Реализованные возможности¶
Предложения (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-перевода |
CALL db.relationshipTypes() |
✓ | Возвращает типы связей из модели |
CALL db.propertyKeys() |
✓ | Возвращает все имена ключей свойств по всем типам узлов |
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 [list] |
✓ | 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]) |
Свойства-объекты. Столбец с объявленным объектом (встроенным объектом удалённого типа GraphQL или любым столбцом, зарегистрированным с полями объекта) — это свойство-словарь: RETURN n.address возвращает его как словарь, в каком бы виде его ни вернул движок, а узел n несёт его среди своих свойств. Поле удалённого GraphQL, указывающее на зарегистрированный тип, один объект или список, не является свойством; до него добираются по его связи.
Функции агрегации¶
| 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"). Правила:
- Переменная внешней области видимости (
x) должна присутствовать вWITH - Поддерживается несколько импортированных переменных (
WITH a, b) - Первая связь во внутреннем MATCH, чей источник — переменная, связанная через lateral, определяет внутренний
FROMи условие соединения - Некоррелированные блоки
CALL { ... }верхнего уровня (безWITH) обрабатываются черезcypher_calls_to_sql_list
Записи (Writes)¶
Cypher поддерживает три паттерна записи через эндпоинт /data/cypher, выполняемые provisa/cypher/write_translator.py.
| Cypher | SQL |
|---|---|
CREATE (n:Label {props}) |
INSERT INTO catalog.schema.table (cols) VALUES (vals) |
MATCH (n:Label) WHERE … DELETE n |
DELETE FROM catalog.schema.table WHERE … |
MATCH (n:Label) WHERE … SET n.prop = val, … |
UPDATE catalog.schema.table SET col = val, … WHERE … |
Имена свойств отображаются на столбцы через удаление доменного префикса и разрешение алиасов; скалярные значения Cypher приводятся к типу целевого столбца. Тело ответа содержит счётчик affected_rows.
Свойство узла — это опубликованное имя его столбца: псевдоним столбца, если он есть, иначе имя по соглашению об именовании — имя, под которым его читают GraphQL и SQL, — одинаково в MATCH, RETURN, CREATE, SET и MERGE.
Правила:
- Метка должна разрешаться ровно в одну зарегистрированную таблицу. Неоднозначные или неизвестные метки — это жёсткая ошибка; нечёткое сопоставление не производится. Новые метки или типы нельзя создать через Cypher.
- Каждая запись проверяется по разрешениям на запись целевой таблицы: роли нужны право
writeи разрешение на операцию, а записываемые колонки должны быть указаны в разрешении. Роль без них отклоняется на этапе компиляции. - Базовый коннектор источника должен поддерживать DML. Источники только для чтения (Trino-федерированные, Iceberg без коннектора Delta) отклоняют записи на этапе перевода.
- Связи нельзя записывать: они выводятся из соединений, объявленных в модели, а не хранятся как рёбра. Обращение к связи как к цели — жёсткая ошибка. Ребро через junction не исключение: стоящая за ним ассоциативная таблица сама является зарегистрированной таблицей, и строки пишутся именно в неё, а не в ребро.
- Записи проходят через полный конвейер записи: внедрение RLS и пост-мутационные хуки (инвалидация кеша ответов, пометка материализованного представления как устаревшего, события изменений Kafka, перезагрузка горячей таблицы).
MERGE,DETACH DELETEиREMOVEне поддерживаются и отклоняются на этапе разбора.
Доступ по протоколу¶
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. Зарегистрированные команды также вызываемы здесь черезCALL fn(args) YIELD col1, col2, и позиционные аргументы отображаются на объявленные имена аргументов команды по порядку. - Bolt — сервер бинарного протокола, совместимого с Neo4j (кодек PackStream, фреймирование с разбиением на части), который позволяет Neo4j Browser, Bloom и драйверам Bolt выполнять Cypher по федеративному графу. Он запускается, когда
PROVISA_BOLT_PORTустановлен в ненулевое значение, и отключён по умолчанию; установитеPROVISA_BOLT_CERT/PROVISA_BOLT_KEYдля TLS. Аутентификация Bolt отображает принципала на пользователя, а базу данных — на роль:SHOW DATABASESперечисляет по одной записи на пару (представление × роль), названнуюprovisa_<role>(бизнес-домены) илиprovisa_ops_<role>(с доменами system/meta/ops);:useвыбирает активную роль и представление. Связи получают устойчивые целочисленные ID через таблицуrel_ids, зеркалируя дизайнnode_ids. Зарегистрированные команды вызываемы черезCALL command(args), и позиционные аргументы отображаются на объявленные имена аргументов по порядку; процедурыCALL dbms.*/CALL db.*имеют приоритет.
Аналитика графов¶
POST /data/graph-analytics выполняет запрос Cypher, строит граф NetworkX в памяти из полученных узлов и рёбер, выполняет именованный алгоритм и вливает словарь _analytics в каждый узел и ребро перед возвратом их как JSON с полем elapsed_ms. Ключи _analytics различаются по алгоритму: центральность даёт score; выявление сообществ даёт cluster; k-core даёт core_number; центральность по степени добавляет in_degree и out_degree. Эндпоинт отклоняет графы выше настраиваемого размера (по умолчанию 10 000 узлов / 50 000 рёбер) с HTTP 413; Girvan-Newman ограничен 500 узлами, если вызывающая сторона не передаёт force=true.
Ограничения¶
Проектные ограничения¶
-
Записи ограничены
CREATE,SETиDELETE. Они выполняются как прямые записи в таблицы через тот же конвейер, что и мутации GraphQL и SQL. См. §Записи выше.MERGE,DETACH DELETEиREMOVEотклоняются на этапе разбора. Процедуры APOC также отклоняются. -
Свойства связи есть только у рёбер через junction. Ребро, объявленное над парой столбцов внешнего ключа, существует в модели только как метаданные соединения и не хранит атрибутов, поэтому
WHERE r.since > 2020илиRETURN r.weightдля него не имеют смысла. Ребро, объявленное над связующей таблицей, атрибуты несёт. Остальные столбцы ассоциативной таблицы являются свойствами связи, иRETURN rвозвращает их.WHEREпо одному из них компилируется в предикат на псевдониме связующей таблицы, поэтому он ограничивает сам обход, а не фильтрует уже собранные строки. Связующая таблица убирается со стороны узлов схемы графа: здесь она ребро, а везде ещё таблица. -
Двунаправленный обход
(a)-[]-(b)переписывается в UNION ALL прямого и обратного направлений каждой подходящей направленной связи модели. У каждой связи в модели есть направление, а двунаправленный синтаксис служит сокращением, которое раскрывается в оба. Дополнительные ветви создаются на самом внешнем уровне запроса. Последующий шаблон MATCH в том же запросе не дублируется по ветвям, что ограничивает двунаправленные шаблоны в запросе с несколькими предложениями MATCH. -
Рекурсивные пути требуют границы. Паттерны переменной длины (
[*]) должны включать верхнюю границу (например,[*..10]). Неограниченный обход отклоняется на этапе разбора для предотвращения неконтролируемых рекурсивных CTE.
Замечания о поведении¶
-
shortestPathна не самоссылающихся путях использует плоский JOIN без упорядочивания по hops. Когда начальный и конечный типы различаются и в схеме нет самоссылающейся связи, транслятор порождает плоскую цепочку JOIN по кратчайшему пути схемы. Этот путь кода не отслеживает hops, поэтомуORDER BY hopsне порождается. Результатом будет структурно кратчайший путь схемы. Он может отличаться от кратчайшего пути через данные по нескольким строкам. -
Несколько путей схемы производят
UNION ALL. Когда два пути схемы с одинаковым числом переходов соединяют одни и те же начальный и конечный типы (например,Person -[WORKS_AT]-> CompanyиPerson -[MANAGES]-> Company), оба выдаются как ветвиUNION ALL. Дедупликация строк, появляющихся в обеих ветвях, не выполняется. -
Одна
RelationshipMappingна пару источник→цель и комбинацию rel_type. Если два поля GraphQL на одном исходном типе производят одну и ту же строкуrel_type(после приведения к верхнему регистру) для одного и того же целевого типа, вторая регистрация перезаписывает первую вCypherLabelMap.relationships. Ключ связи включает имена исходного и целевого типов, поэтому различные пары источник/цель с одинаковым именем типа получают собственные записи и не затрагиваются. -
CTE предложения
WITHназываются_w0,_w1, … Имена назначаются позиционно в рамках одного вызова перевода. Композиция нескольких переведённых запросов (например, в пакете) может привести к коллизиям имён CTE, если они конкатенируются наивно.
Покрытие выражений и паттернов¶
Выражения 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пишет строки в такую таблицу (см. §Writes) и не может определить новую метку или тип. Принимаются обе формы метки, и они означают одну и ту же проверку: постфиксная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(...)или сразу включение по паттерну.