Supporto query Cypher¶
Provisa traduce un sottoinsieme di openCypher in SQL tramite il modulo provisa/cypher/. (REQ-345, REQ-347) Le query vengono analizzate da un parser custom recursive-descent (nessuna libreria Cypher esterna) (REQ-571), risolte contro il layer semantico (REQ-351), ed emesse come SQL, poi instradate al motore di esecuzione target. (REQ-066, REQ-067, REQ-347)
Funzionalità implementate¶
Clausole¶
| Clausola | Stato | Note |
|---|---|---|
MATCH (n:Label) |
✓ | Pattern nodo con etichette, variabili, proprietà inline |
OPTIONAL MATCH |
✓ | Emette LEFT JOIN |
WHERE |
✓ | Supporto completo di espressioni; applicato dopo MATCH |
RETURN |
✓ | Star, accesso proprietà, espressioni, alias |
RETURN DISTINCT |
✓ | Emette SELECT DISTINCT |
WITH |
✓ | Emette una CTE nominata (_w0, _w1, …); supporta WITH … WHERE |
ORDER BY |
✓ | ASC / DESC |
SKIP / LIMIT |
✓ | Mappa a SQL OFFSET / LIMIT |
UNION / UNION ALL |
✓ | Union ricorsiva tra sotto-AST |
CALL { … } |
✓ | Decomposizione della subquery call top-level via cypher_calls_to_sql_list |
CALL { WITH x … } |
✓ | Subquery correlata → CROSS JOIN LATERAL; vedi §CALL correlata |
CALL db.labels() |
✓ | Restituisce le etichette dei nodi dal layer semantico; nessuna traduzione SQL (REQ-572) |
CALL db.relationshipTypes() |
✓ | Restituisce i tipi di relazione dal layer semantico (REQ-572) |
CALL db.propertyKeys() |
✓ | Restituisce tutti i nomi delle chiavi di proprietà su tutti i tipi nodo (REQ-572) |
UNWIND |
✓ | Espansione array-a-righe; il primo elemento diventa FROM, i successivi diventano CROSS JOIN UNNEST |
Pattern Match¶
| Pattern | Stato | Note |
|---|---|---|
(n) — nodo senza etichetta |
✓ | UNION ALL su tutti i tipi conosciuti |
(n:Label) |
✓ | Mappa alla tabella registrata per quel tipo GraphQL |
(n:Label {prop: val}) |
✓ | Filtro proprietà inline diventa WHERE |
(a)-[:TYPE]->(b) |
✓ | Diretto, singolo hop |
(a)<-[:TYPE]-(b) |
✓ | Attraversamento all'indietro; colonne di join invertite |
(a)-[]->(b) |
✓ | Qualsiasi relazione diretta a→b; UNION ALL se più tipi corrispondono |
(a)-[]-(b) |
✓ | Bidirezionale; si espande a UNION ALL di tutte le relazioni forward e backward |
(a)-[:TYPE*..N]->(b) |
✓ | Lunghezza variabile con limite superiore; CTE ricorsiva per self-referenziale, JOIN piatto altrimenti |
(a)-[]->(b)-[]->(c) |
✓ | JOIN concatenati multi-hop |
(n:DomainLabel) |
✓ | Etichetta di dominio → subquery UNION ALL su tutti i tipi nel dominio |
(n:A\|B) |
✓ | Alternanza di etichette → dominio ad-hoc iniettato nella mappa etichette; UNION ALL sui tipi corrispondenti |
shortestPath(…) |
✓ | JOIN piatto per endpoint eterogenei; CTE WITH RECURSIVE per stesso-tipo/self-referenziale |
allShortestPaths(…) |
✓ | Come shortestPath senza LIMIT 1 |
Espressioni e predicati¶
| Funzionalità | Stato | Mapping SQL |
|---|---|---|
Accesso proprietà n.prop |
✓ | n."prop" |
Parametri $name |
✓ | Posizionale $N |
Parametri legacy {name} |
✓ | Normalizzato a $name al momento del parsing |
Confronto =, <>, <, >, <=, >= |
✓ | Diretto |
AND, OR, NOT |
✓ | Diretto |
IS NULL / IS NOT NULL |
✓ | Diretto |
IN [list] |
✓ | SQL IN; sintassi a parentesi quadre [...] di Cypher riscritta come (...) |
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 … } |
✓ | Subquery correlata EXISTS (SELECT 1 FROM …) |
COUNT { MATCH … } |
✓ | Subquery correlata (SELECT count(*) FROM …) |
COLLECT { MATCH … RETURN x } |
✓ | Subquery correlata ARRAY(SELECT x FROM …) |
id(n) |
✓ | Risolto alla colonna ID configurata del nodo |
labels(n) |
✓ | ARRAY['Label'] |
keys(n) |
✓ | ARRAY['prop1', 'prop2', …] |
type(r) |
✓ | Risolto a tempo di compilazione a un literal stringa 'REL_TYPE'; nessuna colonna runtime |
length(p) |
✓ | _t.hops per path CTE ricorsivi; 1 per path JOIN piatto |
CASE WHEN … THEN … ELSE … END |
✓ | Diretto (forme searched e simple) |
| GROUP BY implicito | ✓ | Gli elementi RETURN non aggregati diventano chiavi GROUP BY quando un elemento qualsiasi ha un aggregato |
Proiezioni map¶
| Sintassi | Mapping SQL |
|---|---|
n { .prop1, .prop2 } |
MAP(ARRAY['prop1','prop2'], ARRAY[n."prop1",n."prop2"]) |
n { .* } |
MAP(ARRAY[all props...], ARRAY[n."col",...]) — espanso dallo schema |
n { .*, extra: expr } |
Tutte le proprietà dello schema più chiave nominata; MAP combinata |
n { key: expr } |
MAP(ARRAY['key'], ARRAY[expr]) |
Funzioni di aggregazione¶
| Cypher | SQL |
|---|---|
count(*), count(x) |
diretto |
count(DISTINCT x) |
count(DISTINCT x) |
collect(x) |
array_agg(x) |
avg, sum, min, max |
diretto |
stDev(x) |
stddev_samp(x) |
stDevP(x) |
stddev_pop(x) |
percentileCont(x, p) |
approx_percentile(x, p) |
percentileDisc(x, p) |
approx_percentile(x, p) |
Funzioni stringa¶
| Cypher | SQL |
|---|---|
toLower(x) |
lower(x) |
toUpper(x) |
upper(x) |
ltrim(x), rtrim(x), trim(x) |
diretto |
replace(x, a, b) |
diretto |
reverse(x) |
diretto |
split(x, d) |
diretto |
left(x, n) |
left(x, n) |
right(x, n) |
right(x, n) |
substring(x, start, len) |
substr(x, start+1, len) (indice 0→1) |
size(string) |
char_length(string) |
size(list) |
cardinality(list) |
Funzioni di conversione 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 |
varianti TRY_CAST |
Funzioni matematiche¶
| Cypher | SQL |
|---|---|
log(x) |
ln(x) (logaritmo naturale) |
log2(x) |
log2(x) |
range(start, end) |
sequence(start, end) |
abs, sqrt, ceil, floor, round, sign |
passate direttamente |
Funzioni 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 |
List Comprehension¶
| Sintassi | Mapping 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 Comprehension¶
| Sintassi | Mapping SQL |
|---|---|
[(a)-[:R]->(b) \| b.prop] |
ARRAY(SELECT b."prop" FROM ... WHERE a.fk = b.pk) |
[(a)-[]->(b:Label) \| b.prop] |
tipo inferito dal layer semantico; stessa forma subquery ARRAY |
Subquery CALL correlate¶
CALL { WITH x MATCH (x)-[:R]->(n) RETURN n.prop AS alias } si traduce in CROSS JOIN LATERAL (SELECT n."prop" AS alias FROM ... WHERE x."pk" = n."fk"). (REQ-573) Regole:
- La variabile di scope esterno (
x) deve comparire inWITH - Sono supportate più variabili importate (
WITH a, b) - La prima relazione nel MATCH interno la cui sorgente è una variabile lateral-bound determina il
FROMinterno e la condizione di join - I blocchi
CALL { ... }top-level non correlati (senzaWITH) sono gestiti dacypher_calls_to_sql_list
Scritture¶
Cypher supporta tre pattern di scrittura tramite l'endpoint /data/cypher, eseguiti da 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 |
I nomi delle proprietà mappano a colonne tramite lo strip del prefisso di dominio e la risoluzione degli alias; i valori scalari Cypher vengono convertiti al tipo della colonna target. (REQ-666, REQ-668) Il corpo della risposta porta un conteggio affected_rows. (REQ-670)
Regole:
- L'etichetta deve risolvere esattamente a una tabella registrata. Etichette ambigue o sconosciute sono errori bloccanti; nessun matching fuzzy. (REQ-661) Non è possibile creare nuove etichette o tipi tramite Cypher. (REQ-662)
- Ogni scrittura è vincolata alla ACL
writable_bydella tabella target; un ruolo senza diritti di scrittura viene rifiutato a tempo di compilazione. (REQ-663) - Il connettore dell'origine sottostante deve supportare DML. Le origini di sola lettura (federate via Trino, Iceberg senza connettore Delta) rifiutano le scritture a tempo di traduzione. (REQ-664)
- Le relazioni non possono essere scritte — sono derivate da join su chiave esterna, non archi memorizzati. Targettare una relazione è un errore bloccante. (REQ-665)
- Le scritture passano attraverso l'intera pipeline di scrittura: iniezione RLS e hook post-mutation (invalidazione della cache di risposta, marcatura stale delle viste materializzate, eventi di modifica Kafka, ricarica hot-table). (REQ-798)
MERGE,DETACH DELETE, eREMOVEnon sono supportati e vengono rifiutati a tempo di parsing. (REQ-671)
Accesso al protocollo¶
Cypher raggiunge la stessa pipeline governata su due trasporti:
- HTTP —
POST /data/cyphercon un corpo JSON ({"query": "...", "params": {...}}). Restituisce righe tipizzate, oaffected_rowsper le scritture. Le variabili grafo nella clausolaRETURNvengono serializzate come JSON: i nodi portanoid,label,tableLabel, eproperties; gli archi portanoidentity,start,end,type,properties,startNode, eendNode; i path portanonodes,edges, elength/hops. (REQ-750) I comandi registrati sono anche invocabili qui viaCALL fn(args) YIELD col1, col2— gli argomenti posizionali mappano ai nomi degli argomenti dichiarati del comando in ordine. (REQ-1156) [tool-verified:provisa/api/rest/registered_call.py:113-143] - Bolt — un server protocollo binario compatibile Neo4j (codec PackStream, framing chunked) che permette a Neo4j Browser, Bloom, e driver Bolt di eseguire Cypher sul grafo federato. (REQ-802) Si avvia quando
PROVISA_BOLT_PORTè impostato a un valore diverso da zero ed è disabilitato per default; impostaPROVISA_BOLT_CERT/PROVISA_BOLT_KEYper TLS. [tool-verified:provisa/api/app_startup.py:317-338] L'autenticazione Bolt mappa il principal a un utente e il database a un ruolo:SHOW DATABASESelenca una voce per ogni coppia (vista × ruolo), nominataprovisa_<role>(domini business) oprovisa_ops_<role>(con domini system/meta/ops);:useseleziona il ruolo e la vista attivi. (REQ-807) Le relazioni ricevono ID interi durevoli tramite una tabellarel_ids, rispecchiando il design dinode_ids. (REQ-806) I comandi registrati sono invocabili conCALL command(args)— gli argomenti posizionali mappano ai nomi degli argomenti dichiarati in ordine; le procedureCALL dbms.*/CALL db.*hanno la precedenza. (REQ-1156) [tool-verified:provisa/bolt/session.py:722-749]
Graph Analytics¶
POST /data/graph-analytics esegue una query Cypher, costruisce un grafo NetworkX in-memory dai nodi e archi risultanti, esegue un algoritmo nominato, e unisce un dict _analytics in ogni nodo e arco prima di restituirli come JSON con un campo elapsed_ms. (REQ-642) Le chiavi _analytics variano per algoritmo: la centralità produce score; il rilevamento di community produce cluster; il k-core produce core_number; la centralità di grado aggiunge in_degree e out_degree. (REQ-643) L'endpoint rifiuta grafi sopra una dimensione configurabile (default 10.000 nodi / 50.000 archi) con HTTP 413; Girvan-Newman è limitato a 500 nodi a meno che il chiamante non passi force=true. (REQ-650, REQ-651)
Limitazioni¶
Vincoli di design¶
-
Le scritture sono limitate a
CREATE,SET, eDELETE. Queste vengono eseguite come scritture dirette su tabella attraverso la stessa pipeline delle mutation GraphQL e SQL. (REQ-818, REQ-666, REQ-667, REQ-668) Vedi §Scritture sopra.MERGE,DETACH DELETE, eREMOVEvengono rifiutati a tempo di parsing. (REQ-671, REQ-818) Anche le procedure APOC vengono rifiutate. -
Nessuna proprietà di relazione. Le relazioni (
-[r:TYPE]->) esistono solo come metadati di join nel layer semantico. (REQ-574) Non portano attributi memorizzati, quindiWHERE r.since > 2020oRETURN r.weightnon hanno significato e non sono supportati. -
L'attraversamento bidirezionale
(a)-[]-(b)si riscrive nella UNION ALL forward+backward di tutte le relazioni dirette corrispondenti dal layer semantico. (REQ-575) Ogni relazione nel layer semantico è direzionale; la sintassi bidirezionale è zucchero sintattico che si espande in entrambe le direzioni. I rami extra vengono emessi al livello di query più esterno — i pattern MATCH successivi nella stessa query non vengono duplicati tra i rami (limitazione per bidirezionale multi-MATCH). -
I path ricorsivi richiedono un limite. I pattern a lunghezza variabile (
[*]) devono includere un limite superiore (es.[*..10]). (REQ-348) L'attraversamento illimitato viene rifiutato a tempo di parsing per prevenire CTE ricorsive fuori controllo.
Note sul comportamento¶
-
shortestPathsu path non self-referenziali usa JOIN piatto, non ordinamento per hop. Quando i tipi di inizio e fine differiscono e non esiste alcuna relazione self-referenziale nello schema, il traduttore emette una catena di JOIN piatta (il path schema più breve). (REQ-576) Non emetteORDER BY hopsperché gli hop non sono tracciati in quel percorso di codice. Il risultato è il path schema strutturalmente più breve, non il path data-più-breve su più righe. -
Path schema multipli producono
UNION ALL. Quando due path schema con lo stesso numero di hop collegano gli stessi tipi di inizio e fine (es.Person -[WORKS_AT]-> CompanyePerson -[MANAGES]-> Company), entrambi vengono emessi come ramiUNION ALL. (REQ-577) La deduplicazione delle righe che compaiono in entrambi i rami non viene eseguita. -
Un
RelationshipMappingper ogni combinazione coppia sorgente→target e rel_type. Se due campi GraphQL sullo stesso tipo sorgente producono la stessa stringarel_type(dopo l'uppercasing) verso lo stesso tipo target, la seconda registrazione sovrascrive la prima inCypherLabelMap.relationships. La chiave di relazione include i nomi dei tipi sorgente e target, quindi coppie sorgente/target distinte con lo stesso nome di tipo ottengono ciascuna la propria voce e non ne sono influenzate. -
Le CTE della clausola
WITHsono nominate_w0,_w1, … (REQ-578) I nomi vengono assegnati posizionalmente all'interno di una singola chiamata di traduzione. Comporre più query tradotte (es. in un batch) può produrre nomi CTE in collisione se vengono concatenate in modo ingenuo.
Copertura di espressioni e pattern (REQ-913)¶
Le espressioni Cypher vengono analizzate in un AST e abbassate nodo-a-nodo a SQL (provisa/cypher/expr_parser.py, provisa/cypher/expr_visitor.py). La grammatica segue la torre di precedenza oC_Expression di openCypher. Supportati: literal, parametri, accesso proprietà, n.prop, indice e slice, aritmetica (+ - * / % ^), confronto, IN, STARTS WITH / ENDS WITH / CONTAINS / =~, IS [NOT] NULL, booleani AND / OR / XOR / NOT, CASE, literal lista e map, list e pattern comprehension (incluso il binding di path p = (…)), proiezione map, reduce, i quantificatori all / any / none / single, subquery esistenziali, e chiamate a funzione.
-
Le etichette sono fisse; non è possibile creare tipi oggetto tramite Cypher. Un'etichetta risolve a un dominio conosciuto, un tipo oggetto conosciuto, o un
domain:object_typequalificato — l'insieme chiuso definito dallo schema registrato. Cypher non introduce mai una nuova etichetta o tipo. La creazione di istanze è possibile solo per tipi già definiti all'interno di un'origine dati scrivibile;CREATEscrive righe in tale tabella (vedi §Scritture) ma non può definire una nuova etichetta o tipo. (REQ-662) Entrambe le forme di etichetta sono accettate e significano lo stesso test: la forma postfissan:Labele quella verbosan IS :Label(e la loro negazionen IS NOT :Label). Un'etichetta qualificata si scriven:domain:object_type. -
shortestPatheallShortestPathssono supportati solo dentroMATCH, non come espressioni. In un pattern (MATCH p = shortestPath((a:Person)-[:KNOWS*..5]->(b:Person))) si traducono in una CTEWITH RECURSIVEe richiedono nodi sorgente e target etichettati. Usati in posizione di espressione — per esempioRETURN shortestPath((a)-[*]->(b))oWHERE length(shortestPath((a)-[*]->(b))) < 5— non sono supportati, perché la riscrittura ricorsiva è guidata dalla clausolaMATCHpiuttosto che da una subquery correlata. -
List comprehension,
REDUCE, e quantificatori operano su valori lista; le pattern comprehension attraversano.reduce(...),all/any/none/single(...), e la list comprehension[x IN list | …]operano su un'espressione lista e si abbassano alle funzioni lista higher-order del motore — non attraversano esse stesse il grafo. La comprehension di pattern[(a)-[:R]->(b) WHERE p | e]attraversa invece: il suo pattern grafo viene indirizzato come subquery correlata, quindi è una comprehension la cui sorgente è un attraversamento. Alimenta i risultati di attraversamento nelle forme lista connodes(p)/relationships(p)/collect(...), oppure usa direttamente una pattern comprehension.