Supporto query Cypher¶
Provisa traduce un sottoinsieme di openCypher in SQL tramite il modulo provisa/cypher/. Le query vengono analizzate da un parser custom recursive-descent (nessuna libreria Cypher esterna), risolte contro il modello, ed emesse come SQL, poi instradate al motore di esecuzione target.
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 modello; nessuna traduzione SQL |
CALL db.relationshipTypes() |
✓ | Restituisce i tipi di relazione dal modello |
CALL db.propertyKeys() |
✓ | Restituisce tutti i nomi delle chiavi di proprietà su tutti i tipi nodo |
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]) |
Proprietà oggetto. Una colonna che contiene un oggetto dichiarato (l'oggetto incorporato di un tipo GraphQL remoto, o qualsiasi colonna registrata con campi oggetto) è una proprietà mappa: RETURN n.address la restituisce come mappa, in qualunque forma la restituisca il motore, e il nodo n la porta tra le sue proprietà. Un campo GraphQL remoto che punta a un tipo registrato, un oggetto o una lista, non è una proprietà; si raggiunge attraverso la sua relazione.
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 modello; 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"). 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.
| 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 … |
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. Il corpo della risposta porta un conteggio affected_rows.
La proprietà di un nodo è il nome pubblicato della sua colonna: l'alias della colonna quando ne ha uno, altrimenti il nome secondo la convenzione di denominazione — il nome con cui la leggono GraphQL e SQL — in MATCH, RETURN, CREATE, SET e MERGE allo stesso modo.
Regole:
- L'etichetta deve risolvere esattamente a una tabella registrata. Etichette ambigue o sconosciute sono errori bloccanti; nessun matching fuzzy. Non è possibile creare nuove etichette o tipi tramite Cypher.
- Ogni scrittura è vincolata alle autorizzazioni di scrittura della tabella target: il ruolo ha bisogno del diritto
writee di un'autorizzazione per l'operazione, e le colonne scritte devono essere nell'autorizzazione. Un ruolo che non le ha viene rifiutato a tempo di compilazione. - 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.
- Le relazioni non possono essere scritte: sono derivate dai join dichiarati nel modello, non archi memorizzati. Targettare una relazione è un errore bloccante. Un arco basato su una tabella di giunzione non fa eccezione: la tabella associativa che lo sostiene è essa stessa una tabella registrata, e le righe si scrivono in quella tabella, non nell'arco.
- 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).
MERGE,DETACH DELETE, eREMOVEnon sono supportati e vengono rifiutati a tempo di parsing.
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. I comandi registrati sono anche invocabili qui viaCALL fn(args) YIELD col1, col2, e gli argomenti posizionali mappano ai nomi degli argomenti dichiarati del comando in ordine. - 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. Si avvia quando
PROVISA_BOLT_PORTè impostato a un valore diverso da zero ed è disabilitato per default; impostaPROVISA_BOLT_CERT/PROVISA_BOLT_KEYper TLS. 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. Le relazioni ricevono ID interi durevoli tramite una tabellarel_ids, rispecchiando il design dinode_ids. I comandi registrati sono invocabili conCALL command(args), e gli argomenti posizionali mappano ai nomi degli argomenti dichiarati in ordine; le procedureCALL dbms.*/CALL db.*hanno la precedenza.
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. 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. 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.
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. Vedi §Scritture sopra.MERGE,DETACH DELETE, eREMOVEvengono rifiutati a tempo di parsing. Anche le procedure APOC vengono rifiutate. -
Le proprietà di relazione esistono solo sugli archi basati su una giunzione. Un arco dichiarato su una coppia di colonne di chiave esterna esiste solo come metadato di join nel modello e non memorizza attributi, quindi
WHERE r.since > 2020oRETURN r.weightnon hanno senso su di esso. Un arco dichiarato su una tabella ponte invece li porta. Le colonne restanti della tabella associativa sono le proprietà della relazione, eRETURN rle restituisce. UnWHEREsu una di esse viene compilato come predicato sull'alias della giunzione, quindi restringe l'attraversamento stesso invece di filtrare righe già assemblate. La tabella ponte viene tolta dal lato nodi dello schema del grafo: qui è un arco, ovunque altrove una tabella. -
L'attraversamento bidirezionale
(a)-[]-(b)viene riscritto come UNION ALL in avanti e all'indietro di ogni relazione orientata corrispondente nel modello. Ogni relazione del modello ha una direzione, e la sintassi bidirezionale è zucchero che si espande in entrambe. I rami aggiuntivi vengono emessi al livello più esterno della query. Un pattern MATCH successivo nella stessa query non viene duplicato tra i rami, il che limita i pattern bidirezionali in una query con più clausole MATCH. -
I path ricorsivi richiedono un limite. I pattern a lunghezza variabile (
[*]) devono includere un limite superiore (es.[*..10]). L'attraversamento illimitato viene rifiutato a tempo di parsing per prevenire CTE ricorsive fuori controllo.
Note sul comportamento¶
-
shortestPathsu path non self-referenziali usa un JOIN piatto senza ordinamento per hop. Quando i tipi di partenza e di arrivo differiscono e lo schema non ha relazioni self-referenziali, il traduttore emette una catena di JOIN piatta lungo il path di schema più breve. Quel percorso di codice non traccia gli hop, quindi non emetteORDER BY hops. Il risultato è il path di schema strutturalmente più breve. Può differire dal path più breve attraverso i dati 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. 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, … 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¶
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 si risolve in un dominio noto, un tipo oggetto noto o un
domain:object_typequalificato, l'insieme chiuso definito dallo schema registrato. Cypher non introduce mai una nuova etichetta o un nuovo tipo. Potete creare istanze solo di tipi già definiti in una sorgente dati scrivibile:CREATEscrive righe in una tale tabella (vedi §Writes) e non può definire una nuova etichetta o un nuovo tipo. Entrambe le forme di etichetta sono accettate e indicano lo stesso test, la postfissan:Labele la estesan IS :Label(con la negazionen IS NOT :Label). Un'etichetta qualificata si scriven:domain:object_type. -
shortestPatheallShortestPathsfunzionano solo dentroMATCH. In un pattern (MATCH p = shortestPath((a:Person)-[:KNOWS*..5]->(b:Person))) si traducono in una CTEWITH RECURSIVEe richiedono nodi sorgente e destinazione con etichetta. Non sono supportati in posizione di espressione, per esempioRETURN shortestPath((a)-[*]->(b))oWHERE length(shortestPath((a)-[*]->(b))) < 5. La riscrittura ricorsiva parte dalla clausolaMATCH, e non c'è una sottoquery correlata che possa portarla. -
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 | …]lavorano su un'espressione lista e si riducono alle funzioni lista di ordine superiore del motore. Da sole non percorrono il grafo. La pattern comprehension[(a)-[:R]->(b) WHERE p | e]invece attraversa: il suo pattern di grafo viene trattato come sottoquery correlata, quindi la sua sorgente è un attraversamento. Per alimentare le forme lista con risultati di attraversamento, usatenodes(p)/relationships(p)/collect(...), oppure direttamente una pattern comprehension.