Перейти к содержанию

Поддержка 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 достигает того же управляемого конвейера через два транспорта:

  • HTTPPOST /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)


Ограничения

Проектные ограничения

  1. Записи ограничены CREATE, SET и DELETE. Они выполняются как прямые записи в таблицы через тот же конвейер, что и мутации GraphQL и SQL. (REQ-818, REQ-666, REQ-667, REQ-668) См. §Записи выше. MERGE, DETACH DELETE и REMOVE отклоняются на этапе разбора. (REQ-671, REQ-818) Процедуры APOC также отклоняются.

  2. Нет свойств у связей. Связи (-[r:TYPE]->) существуют исключительно как метаданные соединения в семантическом слое. (REQ-574) Они не несут хранимых атрибутов, поэтому WHERE r.since > 2020 или RETURN r.weight не имеют смысла и не поддерживаются.

  3. Двунаправленный обход (a)-[]-(b) переписывается в UNION ALL прямого и обратного направлений всех совпадающих направленных связей из семантического слоя. (REQ-575) Каждая связь в семантическом слое направленная; двунаправленный синтаксис — это синтаксический сахар, расширяющийся в оба направления. Дополнительные ветви выдаются на самом внешнем уровне запроса — последующие паттерны MATCH в том же запросе не дублируются по ветвям (ограничение для многошагового MATCH с двунаправленностью).

  4. Рекурсивные пути требуют границы. Паттерны переменной длины ([*]) должны включать верхнюю границу (например, [*..10]). (REQ-348) Неограниченный обход отклоняется на этапе разбора для предотвращения неконтролируемых рекурсивных CTE.

Замечания о поведении

  1. shortestPath на непере-само-ссылающихся путях использует плоский JOIN, а не упорядочивание по hops. Когда начальный и конечный типы различаются и в схеме нет само-ссылающейся связи, транслятор выдаёт цепочку плоских JOIN (кратчайший путь схемы). (REQ-576) Он не выдаёт ORDER BY hops, потому что hops не отслеживаются в этом пути кода. Результат — структурно кратчайший путь схемы, а не кратчайший по данным путь среди нескольких строк.

  2. Несколько путей схемы производят UNION ALL. Когда два пути схемы с одинаковым числом переходов соединяют одни и те же начальный и конечный типы (например, Person -[WORKS_AT]-> Company и Person -[MANAGES]-> Company), оба выдаются как ветви UNION ALL. (REQ-577) Дедупликация строк, появляющихся в обеих ветвях, не выполняется.

  3. Одна RelationshipMapping на пару источник→цель и комбинацию rel_type. Если два поля GraphQL на одном исходном типе производят одну и ту же строку rel_type (после приведения к верхнему регистру) для одного и того же целевого типа, вторая регистрация перезаписывает первую в CypherLabelMap.relationships. Ключ связи включает имена исходного и целевого типов, поэтому различные пары источник/цель с одинаковым именем типа получают собственные записи и не затрагиваются.

  4. 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, экзистенциальные подзапросы и вызовы функций.

  1. Метки фиксированы; вы не можете создавать типы объектов через Cypher. Метка разрешается в известный домен, известный тип объекта или квалифицированный domain:object_type — замкнутое множество, определённое зарегистрированной схемой. Cypher никогда не вводит новую метку или тип. Создание экземпляров возможно только для типов, уже определённых в записываемом источнике данных; CREATE записывает строки в такую таблицу (см. §Записи), но не может определить новую метку или тип. (REQ-662) Поддерживаются обе формы меток и означают один и тот же тест: постфиксная n:Label и развёрнутая n IS :Label (и их отрицание n IS NOT :Label). Квалифицированная метка записывается как n:domain:object_type.

  2. shortestPath и allShortestPaths поддерживаются только внутри MATCH, а не как выражения. В паттерне (MATCH p = shortestPath((a:Person)-[:KNOWS*..5]->(b:Person))) они переводятся в CTE WITH RECURSIVE и требуют помеченных исходного и целевого узлов. При использовании в позиции выражения — например, RETURN shortestPath((a)-[*]->(b)) или WHERE length(shortestPath((a)-[*]->(b))) < 5 — они не поддерживаются, потому что рекурсивное переписывание управляется предложением MATCH, а не коррелированным подзапросом.

  3. Списковые включения, REDUCE и квантификаторы работают со значениями списков; включения по паттернам обходят граф. reduce(...), all/any/none/single(...) и списковое включение [x IN list | …] работают над выражением списка и понижаются до функций списков высшего порядка движка — сами по себе они не обходят граф. Паттерновое включение [(a)-[:R]->(b) WHERE p | e] действительно обходит граф: его графовый паттерн адресуется как коррелированный подзапрос, поэтому это включение, источник которого — обход. Передавайте результаты обхода в списковые формы через nodes(p) / relationships(p) / collect(...), либо используйте паттерновое включение напрямую.