Ir para o conteúdo

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 em WITH
  • 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 FROM interno e a condição de join
  • Blocos CALL { ... } de nível superior não correlacionados (sem WITH) são tratados por cypher_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 write e 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, e REMOVE nã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/cypher com um corpo JSON ({"query": "...", "params": {...}}). Retorna linhas tipadas, ou affected_rows para escritas. Variáveis de grafo na cláusula RETURN serializam como JSON: nós carregam id, label, tableLabel, e properties; arestas carregam identity, start, end, type, properties, startNode, e endNode; caminhos carregam nodes, edges, e length/hops. Commands registrados também são chamáveis aqui via CALL 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; defina PROVISA_BOLT_CERT / PROVISA_BOLT_KEY para TLS. A autenticação Bolt mapeia principal para usuário e banco de dados para função: SHOW DATABASES lista uma entrada por par (view × função), nomeada provisa_<role> (domínios de negócio) ou provisa_ops_<role> (com domínios system/meta/ops); :use seleciona a função e view ativas. Relacionamentos recebem IDs inteiros duráveis via uma tabela rel_ids, espelhando o design de node_ids. Commands registrados são chamáveis com CALL command(args), e args posicionais mapeiam para nomes de argumento declarados em ordem; procedimentos CALL 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

  1. Escritas são limitadas a CREATE, SET, e DELETE. Estas executam como escritas de tabela diretas através do mesmo pipeline que mutações GraphQL e SQL. Veja §Escritas acima. MERGE, DETACH DELETE, e REMOVE são rejeitados no momento da análise. Procedimentos APOC também são rejeitados.

  2. 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 > 2020 ou RETURN r.weight nã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, e RETURN r as devolve. Um WHERE sobre 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.

  3. 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.

  4. 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

  1. shortestPath em 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 emite ORDER BY hops. O resultado é o caminho de esquema estruturalmente mais curto. Ele pode diferir do caminho mais curto pelos dados em várias linhas.

  2. 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]-> Company e Person -[MANAGES]-> Company), ambos são emitidos como branches UNION ALL. Deduplicação de linhas que aparecem em ambos os branches não é realizada.

  3. Um RelationshipMapping por par fonte→alvo e combinação de rel_type. Se dois campos GraphQL no mesmo tipo de fonte produzem a mesma string rel_type (após maiusculização) para o mesmo tipo alvo, o segundo registro sobrescreve o primeiro em CypherLabelMap.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.

  4. CTEs de cláusula WITH sã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.

  1. 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_type qualificado, 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: CREATE escreve 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-fixa n:Label e a verbosa n IS :Label (com a negação n IS NOT :Label). Um label qualificado se escreve n:domain:object_type.

  2. shortestPath e allShortestPaths funcionam só dentro de MATCH. Num padrão (MATCH p = shortestPath((a:Person)-[:KNOWS*..5]->(b:Person))) eles se traduzem numa CTE WITH RECURSIVE e exigem nós de origem e destino com label. Não são suportados em posição de expressão, por exemplo RETURN shortestPath((a)-[*]->(b)) ou WHERE length(shortestPath((a)-[*]->(b))) < 5. A reescrita recursiva parte da cláusula MATCH, e não há subconsulta correlacionada que a carregue.

  3. 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, use nodes(p) / relationships(p) / collect(...), ou use diretamente uma compreensão de padrão.