Suporte a Consulta Cypher¶
O Provisa traduz um subconjunto do openCypher para SQL via o módulo provisa/cypher/. (REQ-345, REQ-347) Consultas são analisadas por um parser recursivo-descendente personalizado (nenhuma biblioteca Cypher externa) (REQ-571), resolvidas de esquema contra a camada semântica (REQ-351), e emitidas como SQL, depois roteadas para o motor de execução alvo. (REQ-066, REQ-067, REQ-347)
Funcionalidades Implementadas¶
Cláusulas¶
| Cláusula | Status | Notas |
|---|---|---|
MATCH (n:Label) |
✓ | Padrões de nó com labels, variáveis, propriedades inline |
OPTIONAL MATCH |
✓ | Emite LEFT JOIN |
WHERE |
✓ | Suporte completo a expressão; aplicado após MATCH |
RETURN |
✓ | Star, acesso a propriedade, expressões, aliases |
RETURN DISTINCT |
✓ | Emite SELECT DISTINCT |
WITH |
✓ | Emite um CTE nomeado (_w0, _w1, …); suporta WITH … WHERE |
ORDER BY |
✓ | ASC / DESC |
SKIP / LIMIT |
✓ | Mapeia para SQL OFFSET / LIMIT |
UNION / UNION ALL |
✓ | União recursiva através de sub-ASTs |
CALL { … } |
✓ | Decomposição de subconsulta de call de nível superior via cypher_calls_to_sql_list |
CALL { WITH x … } |
✓ | Subconsulta correlacionada → CROSS JOIN LATERAL; veja §CALL Correlacionado |
CALL db.labels() |
✓ | Retorna labels de nó da camada semântica; sem tradução SQL (REQ-572) |
CALL db.relationshipTypes() |
✓ | Retorna tipos de relacionamento da camada semântica (REQ-572) |
CALL db.propertyKeys() |
✓ | Retorna todos os nomes de chave de propriedade em todos os tipos de nó (REQ-572) |
UNWIND |
✓ | Expansão de array para linhas; primeiro item se torna FROM, subsequentes se tornam CROSS JOIN UNNEST |
Padrões de Match¶
| Padrão | Status | Notas |
|---|---|---|
(n) — nó sem label |
✓ | UNION ALL sobre todos os tipos conhecidos |
(n:Label) |
✓ | Mapeia para a tabela registrada para aquele tipo GraphQL |
(n:Label {prop: val}) |
✓ | Filtro de propriedade inline se torna WHERE |
(a)-[:TYPE]->(b) |
✓ | Direcionado, hop único |
(a)<-[:TYPE]-(b) |
✓ | Travessia reversa; colunas de join invertidas |
(a)-[]->(b) |
✓ | Qualquer relacionamento direcionado a→b; UNION ALL se múltiplos tipos correspondem |
(a)-[]-(b) |
✓ | Bidirecional; expande para UNION ALL de todos os relacionamentos frente e trás |
(a)-[:TYPE*..N]->(b) |
✓ | Comprimento variável com limite superior; CTE recursivo para auto-referencial, JOIN plano caso contrário |
(a)-[]->(b)-[]->(c) |
✓ | JOINs encadeados de múltiplos hops |
(n:DomainLabel) |
✓ | Label de domínio → subconsulta UNION ALL sobre todos os tipos no domínio |
(n:A\|B) |
✓ | Alternação de label → domínio ad-hoc injetado no mapa de label; UNION ALL sobre tipos correspondentes |
shortestPath(…) |
✓ | JOIN plano para endpoints heterogêneos; CTE WITH RECURSIVE para mesmo-tipo/auto-referencial |
allShortestPaths(…) |
✓ | Igual a shortestPath sem LIMIT 1 |
Expressões e Predicados¶
| Funcionalidade | Status | Mapeamento SQL |
|---|---|---|
Acesso a propriedade n.prop |
✓ | n."prop" |
Parâmetros $name |
✓ | Posicional $N |
Parâmetros legados {name} |
✓ | Normalizado para $name no momento da análise |
Comparação =, <>, <, >, <=, >= |
✓ | Direto |
AND, OR, NOT |
✓ | Direto |
IS NULL / IS NOT NULL |
✓ | Direto |
IN [list] |
✓ | SQL IN; sintaxe de colchetes Cypher [...] reescrita para (...) |
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 … } |
✓ | Subconsulta correlacionada EXISTS (SELECT 1 FROM …) |
COUNT { MATCH … } |
✓ | Subconsulta correlacionada (SELECT count(*) FROM …) |
COLLECT { MATCH … RETURN x } |
✓ | Subconsulta correlacionada ARRAY(SELECT x FROM …) |
id(n) |
✓ | Resolvido para a coluna de ID configurada do nó |
labels(n) |
✓ | ARRAY['Label'] |
keys(n) |
✓ | ARRAY['prop1', 'prop2', …] |
type(r) |
✓ | Resolvido em tempo de compilação para literal string 'REL_TYPE'; sem coluna em runtime |
length(p) |
✓ | _t.hops para caminhos de CTE recursivo; 1 para caminhos de JOIN plano |
CASE WHEN … THEN … ELSE … END |
✓ | Direto (formas buscadas e simples) |
| GROUP BY implícito | ✓ | Itens RETURN não agregados se tornam chaves GROUP BY quando qualquer item tem um agregado |
Projeções de Mapa¶
| Sintaxe | Mapeamento SQL |
|---|---|
n { .prop1, .prop2 } |
MAP(ARRAY['prop1','prop2'], ARRAY[n."prop1",n."prop2"]) |
n { .* } |
MAP(ARRAY[all props...], ARRAY[n."col",...]) — expandido do esquema |
n { .*, extra: expr } |
Todas as props do esquema mais chave nomeada; MAP combinado |
n { key: expr } |
MAP(ARRAY['key'], ARRAY[expr]) |
Funções de Agregação¶
| Cypher | SQL |
|---|---|
count(*), count(x) |
direto |
count(DISTINCT x) |
count(DISTINCT x) |
collect(x) |
array_agg(x) |
avg, sum, min, max |
direto |
stDev(x) |
stddev_samp(x) |
stDevP(x) |
stddev_pop(x) |
percentileCont(x, p) |
approx_percentile(x, p) |
percentileDisc(x, p) |
approx_percentile(x, p) |
Funções de String¶
| Cypher | SQL |
|---|---|
toLower(x) |
lower(x) |
toUpper(x) |
upper(x) |
ltrim(x), rtrim(x), trim(x) |
direto |
replace(x, a, b) |
direto |
reverse(x) |
direto |
split(x, d) |
direto |
left(x, n) |
left(x, n) |
right(x, n) |
right(x, n) |
substring(x, start, len) |
substr(x, start+1, len) (índice 0→1) |
size(string) |
char_length(string) |
size(list) |
cardinality(list) |
Funções de Conversão de Tipo¶
| 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 |
variantes TRY_CAST |
Funções Matemáticas¶
| Cypher | SQL |
|---|---|
log(x) |
ln(x) (log natural) |
log2(x) |
log2(x) |
range(start, end) |
sequence(start, end) |
abs, sqrt, ceil, floor, round, sign |
repassado |
Funções de Lista¶
| 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 |
Compreensões de Lista¶
| Sintaxe | Mapeamento 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) |
Compreensões de Padrão¶
| Sintaxe | Mapeamento SQL |
|---|---|
[(a)-[:R]->(b) \| b.prop] |
ARRAY(SELECT b."prop" FROM ... WHERE a.fk = b.pk) |
[(a)-[]->(b:Label) \| b.prop] |
tipo inferido da camada semântica; mesma forma de subconsulta ARRAY |
Subconsultas CALL Correlacionadas¶
CALL { WITH x MATCH (x)-[:R]->(n) RETURN n.prop AS alias } traduz para CROSS JOIN LATERAL (SELECT n."prop" AS alias FROM ... WHERE x."pk" = n."fk"). (REQ-573) Regras:
- A variável de escopo externo (
x) deve aparecer emWITH - Múltiplas vars importadas (
WITH a, b) são suportadas - O primeiro relacionamento no MATCH interno cuja fonte é uma var vinculada a lateral determina o
FROMinterno e a condição de join - Blocos
CALL { ... }de nível superior não correlacionados (semWITH) são tratados porcypher_calls_to_sql_list
Escritas¶
Cypher suporta três padrões de escrita através do endpoint /data/cypher, executados por 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 |
Nomes de propriedade mapeiam para colunas via remoção de prefixo de domínio e resolução de alias; valores escalares Cypher são coagidos ao tipo de coluna alvo. (REQ-666, REQ-668) O corpo da resposta carrega uma contagem affected_rows. (REQ-670)
Regras:
- O label deve resolver para exatamente uma tabela registrada. Labels ambíguos ou desconhecidos são erros rígidos; sem correspondência fuzzy. (REQ-661) Novos labels ou tipos não podem ser criados através do Cypher. (REQ-662)
- Toda escrita é bloqueada pela ACL
writable_byda tabela alvo; uma função sem direitos de escrita é rejeitada no momento da compilação. (REQ-663) - O conector de fonte subjacente deve suportar DML. Fontes somente-leitura (federadas via Trino, Iceberg sem um conector Delta) rejeitam escritas no momento da tradução. (REQ-664)
- Relacionamentos não podem ser escritos — eles são derivados dos joins declarados na camada semântica, não arestas armazenadas. Direcionar um relacionamento é um erro rígido. (REQ-665) Uma aresta apoiada em junção não é exceção: a tabela associativa por trás dela é ela própria uma tabela registrada, e as linhas são escritas nessa tabela, não na aresta. (REQ-1586)
- Escritas rodam através do pipeline de escrita completo: injeção de RLS e hooks pós-mutação (invalidação de cache de resposta, marcação de obsolescência de view materializada, eventos de mudança Kafka, recarga de tabela quente). (REQ-798)
MERGE,DETACH DELETE, eREMOVEnão são suportados e são rejeitados no momento da análise. (REQ-671)
Acesso por Protocolo¶
Cypher alcança o mesmo pipeline governado através de dois transportes:
- HTTP —
POST /data/cyphercom um corpo JSON ({"query": "...", "params": {...}}). Retorna linhas tipadas, ouaffected_rowspara escritas. Variáveis de grafo na cláusulaRETURNserializam como JSON: nós carregamid,label,tableLabel, eproperties; arestas carregamidentity,start,end,type,properties,startNode, eendNode; caminhos carregamnodes,edges, elength/hops. (REQ-750) Commands registrados também são chamáveis aqui viaCALL fn(args) YIELD col1, col2— args posicionais mapeiam para os nomes de argumento declarados do command em ordem. (REQ-1156) [tool-verified:provisa/api/rest/registered_call.py:113-143] - Bolt — um servidor de protocolo binário compatível com Neo4j (codec PackStream, framing em chunks) que permite ao Neo4j Browser, Bloom, e drivers Bolt rodar Cypher sobre o grafo federado. (REQ-802) Ele inicia quando
PROVISA_BOLT_PORTé definido para um valor não-zero e é desabilitado por padrão; definaPROVISA_BOLT_CERT/PROVISA_BOLT_KEYpara TLS. [tool-verified:provisa/api/app_startup.py:317-338] A autenticação Bolt mapeia principal para usuário e banco de dados para função:SHOW DATABASESlista uma entrada por par (view × função), nomeadaprovisa_<role>(domínios de negócio) ouprovisa_ops_<role>(com domínios system/meta/ops);:useseleciona a função e view ativas. (REQ-807) Relacionamentos recebem IDs inteiros duráveis via uma tabelarel_ids, espelhando o design denode_ids. (REQ-806) Commands registrados são chamáveis comCALL command(args)— args posicionais mapeiam para nomes de argumento declarados em ordem; procedimentosCALL dbms.*/CALL db.*têm precedência. (REQ-1156) [tool-verified:provisa/bolt/session.py:722-749]
Análise de Grafo (Graph Analytics)¶
POST /data/graph-analytics roda uma consulta Cypher, constrói um grafo NetworkX em memória a partir dos nós e arestas resultantes, executa um algoritmo nomeado, e mescla um dict _analytics em cada nó e aresta antes de retorná-los como JSON com um campo elapsed_ms. (REQ-642) As chaves de _analytics variam por algoritmo: centralidade produz score; detecção de comunidade produz cluster; k-core produz core_number; centralidade de grau adiciona in_degree e out_degree. (REQ-643) O endpoint rejeita grafos acima de um tamanho configurável (padrão 10.000 nós / 50.000 arestas) com HTTP 413; Girvan-Newman é limitado a 500 nós a menos que o chamador passe force=true. (REQ-650, REQ-651)
Limitações¶
Restrições de design¶
-
Escritas são limitadas a
CREATE,SET, eDELETE. Estas executam como escritas de tabela diretas através do mesmo pipeline que mutações GraphQL e SQL. (REQ-818, REQ-666, REQ-667, REQ-668) Veja §Escritas acima.MERGE,DETACH DELETE, eREMOVEsão rejeitados no momento da análise. (REQ-671, REQ-818) Procedimentos APOC também são rejeitados. -
Propriedades de relacionamento existem apenas em arestas apoiadas em junção. Uma aresta declarada sobre um par de colunas de chave estrangeira existe somente como metadados de join na camada semântica (REQ-574) e não carrega atributos armazenados, então
WHERE r.since > 2020ouRETURN r.weightnão têm significado sobre ela. Uma aresta declarada sobre uma tabela de junção carrega, sim: as demais colunas da tabela associativa são as propriedades do relacionamento,RETURN ras devolve, e umWHEREsobre uma delas compila para um predicado sobre o alias da junção — portanto restringe a travessia em vez de filtrar linhas já montadas. (REQ-1586) A própria tabela de junção sai do lado dos nós do esquema do grafo; aqui ela é uma aresta e em todo o resto é uma tabela. -
Travessia bidirecional
(a)-[]-(b)reescreve para a UNION ALL frente+trás de todos os relacionamentos direcionados correspondentes da camada semântica. (REQ-575) Todo relacionamento na camada semântica é direcional; sintaxe bidirecional é açúcar sintático que se expande para ambas as direções. Branches extras são emitidos no nível de consulta mais externo — padrões MATCH subsequentes na mesma consulta não são duplicados entre branches (limitação para bidirecional de múltiplos MATCH). -
Caminhos recursivos exigem um limite. Padrões de comprimento variável (
[*]) devem incluir um limite superior (ex.:[*..10]). (REQ-348) Travessia sem limite é rejeitada no momento da análise para prevenir CTEs recursivos descontrolados.
Notas de comportamento¶
-
shortestPathem caminhos não auto-referenciais usa JOIN plano, não ordenação por hops. Quando os tipos de início e fim diferem e nenhum relacionamento auto-referencial existe no esquema, o tradutor emite uma cadeia de JOIN plana (o caminho de esquema mais curto). (REQ-576) Ele não emiteORDER BY hopsporque hops não são rastreados nesse caminho de código. O resultado é o caminho de esquema estruturalmente mais curto, não o caminho mais curto de dados através de múltiplas linhas. -
Múltiplos caminhos de esquema produzem
UNION ALL. Quando dois caminhos de esquema de igual contagem de hop conectam os mesmos tipos de início e fim (ex.:Person -[WORKS_AT]-> CompanyePerson -[MANAGES]-> Company), ambos são emitidos como branchesUNION ALL. (REQ-577) Deduplicação de linhas que aparecem em ambos os branches não é realizada. -
Um
RelationshipMappingpor par fonte→alvo e combinação de rel_type. Se dois campos GraphQL no mesmo tipo de fonte produzem a mesma stringrel_type(após maiusculização) para o mesmo tipo alvo, o segundo registro sobrescreve o primeiro emCypherLabelMap.relationships. A chave de relacionamento inclui nomes de tipo fonte e alvo, então pares fonte/alvo distintos com o mesmo nome de tipo cada um recebe sua própria entrada e não são afetados. -
CTEs de cláusula
WITHsão nomeados_w0,_w1, … (REQ-578) Nomes são atribuídos posicionalmente dentro de uma única chamada de tradução. Compor múltiplas consultas traduzidas (ex.: em um batch) pode produzir nomes de CTE colidentes se forem concatenadas ingenuamente.
Cobertura de expressão e padrão (REQ-913)¶
Expressões Cypher são analisadas em uma AST e reduzidas nó a nó para SQL (provisa/cypher/expr_parser.py, provisa/cypher/expr_visitor.py). A gramática segue a torre de precedência oC_Expression do openCypher. Suportado: literais, parâmetros, acesso a propriedade, n.prop, índice e slice, aritmética (+ - * / % ^), comparação, IN, STARTS WITH / ENDS WITH / CONTAINS / =~, IS [NOT] NULL, booleano AND / OR / XOR / NOT, CASE, literais de lista e mapa, compreensões de lista e padrão (incluindo a vinculação de caminho p = (…)), projeção de mapa, reduce, os quantificadores all / any / none / single, subconsultas existenciais, e chamadas de função.
-
Labels são fixos; você não pode criar tipos de objeto através do Cypher. Um label resolve para um domínio conhecido, um tipo de objeto conhecido, ou um
domain:object_typequalificado — o conjunto fechado definido pelo esquema registrado. Cypher nunca introduz um novo label ou tipo. Criação de instância é possível somente para tipos já definidos dentro de uma fonte de dados gravável;CREATEescreve linhas em tal tabela (veja §Escritas) mas não pode definir um novo label ou tipo. (REQ-662) Ambas as formas de label são aceitas e significam o mesmo teste: o postfixn:Labele o verboson IS :Label(e sua negaçãon IS NOT :Label). Um label qualificado é escriton:domain:object_type. -
shortestPatheallShortestPathssão suportados somente dentro deMATCH, não como expressões. Em um padrão (MATCH p = shortestPath((a:Person)-[:KNOWS*..5]->(b:Person))) eles traduzem para um CTEWITH RECURSIVEe exigem nós de origem e alvo com label. Usados em posição de expressão — por exemploRETURN shortestPath((a)-[*]->(b))ouWHERE length(shortestPath((a)-[*]->(b))) < 5— eles não são suportados, porque a reescrita recursiva é orientada pela cláusulaMATCHem vez de uma subconsulta correlacionada. -
Compreensões de lista,
REDUCE, e quantificadores rodam contra valores de lista; compreensões de padrão percorrem.reduce(...),all/any/none/single(...), e a compreensão de lista[x IN list | …]operam sobre uma expressão de lista e reduzem para as funções de lista de ordem superior do motor — elas mesmas não percorrem o grafo. A compreensão de padrão[(a)-[:R]->(b) WHERE p | e]percorre: seu padrão de grafo é endereçado como uma subconsulta correlacionada, então é uma compreensão cuja fonte é uma travessia. Alimente resultados de travessia nas formas de lista comnodes(p)/relationships(p)/collect(...), ou use uma compreensão de padrão diretamente.