Suporte a Consulta Cypher¶
O Provisa traduz um subconjunto do openCypher para SQL via o módulo provisa/cypher/. Consultas são analisadas por um parser recursivo-descendente personalizado (nenhuma biblioteca Cypher externa), resolvidas de esquema contra o modelo, e emitidas como SQL, depois roteadas para o motor de execução alvo.
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ó do modelo; sem tradução SQL |
CALL db.relationshipTypes() |
✓ | Retorna tipos de relacionamento do modelo |
CALL db.propertyKeys() |
✓ | Retorna todos os nomes de chave de propriedade em todos os tipos de nó |
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]) |
Propriedades de objeto. Uma coluna que contém um objeto declarado (o objeto embutido de um tipo GraphQL remoto, ou qualquer coluna registrada com campos de objeto) é uma propriedade de mapa: RETURN n.address a retorna como mapa, seja qual for a forma em que o motor a devolve, e o nó n a traz entre suas propriedades. Um campo GraphQL remoto que aponta para um tipo registrado, um objeto ou uma lista, não é uma propriedade; é alcançado pelo seu relacionamento.
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 do modelo; 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"). 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.
| 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 … |
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. O corpo da resposta carrega uma contagem affected_rows.
A propriedade de um nó é o nome publicado da sua coluna: o alias da coluna quando ela tem um, senão o nome pela convenção de nomenclatura — o nome pelo qual GraphQL e SQL a leem — em MATCH, RETURN, CREATE, SET e MERGE igualmente.
Regras:
- O label deve resolver para exatamente uma tabela registrada. Labels ambíguos ou desconhecidos são erros rígidos; sem correspondência fuzzy. Novos labels ou tipos não podem ser criados através do Cypher.
- Toda escrita é bloqueada pelas permissões de escrita da tabela alvo: a função precisa do direito
writee de uma permissão para a operação, e as colunas escritas devem estar na permissão. Uma função sem elas é rejeitada no momento da compilação. - 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.
- Relacionamentos não podem ser escritos: eles são derivados dos joins declarados no modelo, não arestas armazenadas. Direcionar um relacionamento é um erro rígido. 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.
- 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).
MERGE,DETACH DELETE, eREMOVEnão são suportados e são rejeitados no momento da análise.
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. Commands registrados também são chamáveis aqui viaCALL fn(args) YIELD col1, col2, e args posicionais mapeiam para os nomes de argumento declarados do command em ordem. - 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. 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. 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. Relacionamentos recebem IDs inteiros duráveis via uma tabelarel_ids, espelhando o design denode_ids. Commands registrados são chamáveis comCALL command(args), e args posicionais mapeiam para nomes de argumento declarados em ordem; procedimentosCALL dbms.*/CALL db.*têm precedência.
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. 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. 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.
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. Veja §Escritas acima.MERGE,DETACH DELETE, eREMOVEsão rejeitados no momento da análise. 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 só como metadado de join no modelo e não guarda atributos, então
WHERE r.since > 2020ouRETURN r.weightnão significam nada nela. Uma aresta declarada sobre uma tabela de junção carrega atributos. As colunas restantes da tabela associativa são as propriedades do relacionamento, eRETURN ras devolve. UmWHEREsobre uma delas compila para um predicado no alias da junção, então restringe a própria travessia em vez de filtrar linhas já montadas. A 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)é reescrita como o UNION ALL para frente e para trás de cada relacionamento dirigido correspondente no modelo. Todo relacionamento do modelo tem direção, e a sintaxe bidirecional é açúcar que se expande para as duas. Os ramos extras são emitidos no nível mais externo da consulta. Um padrão MATCH posterior na mesma consulta não é duplicado entre os ramos, o que limita padrões bidirecionais numa consulta com várias cláusulas MATCH. -
Caminhos recursivos exigem um limite. Padrões de comprimento variável (
[*]) devem incluir um limite superior (ex.:[*..10]). 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 sem ordenação por hops. Quando os tipos de início e fim diferem e o esquema não tem relacionamento auto-referencial, o tradutor emite uma cadeia de JOIN plana ao longo do caminho de esquema mais curto. Esse caminho de código não rastreia hops, então não emiteORDER BY hops. O resultado é o caminho de esquema estruturalmente mais curto. Ele pode diferir do caminho mais curto pelos dados em várias 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. 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, … 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¶
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 que o esquema registrado define. O Cypher nunca introduz um label ou tipo novo. Você só pode criar instâncias de tipos já definidos numa fonte de dados gravável:CREATEescreve linhas numa tabela assim (veja §Writes) e não pode definir um label ou tipo novo. As duas formas de label são aceitas e significam o mesmo teste, a pós-fixan:Labele a verbosan IS :Label(com a negaçãon IS NOT :Label). Um label qualificado se escreven:domain:object_type. -
shortestPatheallShortestPathsfuncionam só dentro deMATCH. Num padrão (MATCH p = shortestPath((a:Person)-[:KNOWS*..5]->(b:Person))) eles se traduzem numa CTEWITH RECURSIVEe exigem nós de origem e destino com label. Não são suportados em posição de expressão, por exemploRETURN shortestPath((a)-[*]->(b))ouWHERE length(shortestPath((a)-[*]->(b))) < 5. A reescrita recursiva parte da cláusulaMATCH, e não há subconsulta correlacionada que a carregue. -
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 | …]trabalham sobre uma expressão de lista e se reduzem às 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 é tratado como subconsulta correlacionada, então sua origem é uma travessia. Para alimentar as formas de lista com resultados de travessia, usenodes(p)/relationships(p)/collect(...), ou use diretamente uma compreensão de padrão.