תמיכה בשאילתות Cypher¶
Provisa מתרגמת תת-קבוצה של openCypher ל-SQL דרך המודול provisa/cypher/. שאילתות מפוענחות על ידי parser recursive-descent מותאם אישית (ללא ספריית Cypher חיצונית), נפתרות-סכמה (schema-resolved) מול המודל, ונפלטות כ-SQL, ואז מנותבות למנוע הביצוע היעד.
תכונות ממומשות¶
Clauses¶
| Clause | סטטוס | הערות |
|---|---|---|
MATCH (n:Label) |
✓ | דפוסי node עם תוויות, משתנים, מאפיינים inline |
OPTIONAL MATCH |
✓ | פולט LEFT JOIN |
WHERE |
✓ | תמיכה מלאה בביטויים; מוחל אחרי MATCH |
RETURN |
✓ | Star, גישת מאפיין, ביטויים, aliases |
RETURN DISTINCT |
✓ | פולט SELECT DISTINCT |
WITH |
✓ | פולט CTE בעל-שם (_w0, _w1, …); תומך ב-WITH … WHERE |
ORDER BY |
✓ | ASC / DESC |
SKIP / LIMIT |
✓ | ממופה ל-SQL OFFSET / LIMIT |
UNION / UNION ALL |
✓ | union רקורסיבי על פני תתי-AST |
CALL { … } |
✓ | פירוק תת-שאילתת call ברמה-עליונה דרך cypher_calls_to_sql_list |
CALL { WITH x … } |
✓ | תת-שאילתה מתואמת (correlated) → CROSS JOIN LATERAL; ראו §CALL מתואם |
CALL db.labels() |
✓ | מחזיר תוויות node מהמודל; ללא תרגום SQL |
CALL db.relationshipTypes() |
✓ | מחזיר סוגי קשר מהמודל |
CALL db.propertyKeys() |
✓ | מחזיר את כל שמות מפתחות המאפיינים על פני כל סוגי ה-node |
UNWIND |
✓ | הרחבת array-לשורות; הפריט הראשון הופך ל-FROM, הבאים הופכים ל-CROSS JOIN UNNEST |
דפוסי Match¶
| דפוס | סטטוס | הערות |
|---|---|---|
(n) — node ללא תווית |
✓ | UNION ALL על פני כל הטיפוסים הידועים |
(n:Label) |
✓ | ממופה לטבלה הרשומה עבור אותו טיפוס GraphQL |
(n:Label {prop: val}) |
✓ | פילטר מאפיין inline הופך ל-WHERE |
(a)-[:TYPE]->(b) |
✓ | מכוון, hop יחיד |
(a)<-[:TYPE]-(b) |
✓ | מעבר אחורה; עמודות join הפוכות |
(a)-[]->(b) |
✓ | כל קשר מכוון a→b; UNION ALL אם מספר טיפוסים תואמים |
(a)-[]-(b) |
✓ | דו-כיווני; מתרחב ל-UNION ALL של כל הקשרים הקדימה והאחורה |
(a)-[:TYPE*..N]->(b) |
✓ | אורך-משתנה עם גבול עליון; CTE רקורסיבי עבור self-referential, JOIN שטוח אחרת |
(a)-[]->(b)-[]->(c) |
✓ | JOIN-ים מרובי-hop משורשרים |
(n:DomainLabel) |
✓ | תווית דומיין → תת-שאילתת UNION ALL על פני כל הטיפוסים בדומיין |
(n:A\|B) |
✓ | חלופת תווית → דומיין אד-הוק מוזרק למפת התוויות; UNION ALL על פני טיפוסים תואמים |
shortestPath(…) |
✓ | JOIN שטוח עבור endpoints הטרוגניים; CTE WITH RECURSIVE עבור אותו-טיפוס/self-referential |
allShortestPaths(…) |
✓ | כמו shortestPath ללא LIMIT 1 |
ביטויים ופרדיקטים¶
| תכונה | סטטוס | מיפוי SQL |
|---|---|---|
גישת מאפיין n.prop |
✓ | n."prop" |
פרמטרים $name |
✓ | פוזיציוני $N |
פרמטרים ישנים {name} |
✓ | מנורמל ל-$name בזמן פענוח |
השוואה =, <>, <, >, <=, >= |
✓ | ישיר |
AND, OR, NOT |
✓ | ישיר |
IS NULL / IS NOT NULL |
✓ | ישיר |
IN [list] |
✓ | SQL IN; תחביר סוגריים [...] של Cypher נכתב מחדש ל-(...) |
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 … } |
✓ | תת-שאילתת EXISTS (SELECT 1 FROM …) מתואמת |
COUNT { MATCH … } |
✓ | תת-שאילתת (SELECT count(*) FROM …) מתואמת |
COLLECT { MATCH … RETURN x } |
✓ | תת-שאילתת ARRAY(SELECT x FROM …) מתואמת |
id(n) |
✓ | נפתר לעמודת ה-ID המוגדרת של ה-node |
labels(n) |
✓ | ARRAY['Label'] |
keys(n) |
✓ | ARRAY['prop1', 'prop2', …] |
type(r) |
✓ | נפתר בזמן קומפילציה למחרוזת-ליטרל 'REL_TYPE'; ללא עמודת runtime |
length(p) |
✓ | _t.hops עבור נתיבי CTE רקורסיביים; 1 עבור נתיבי JOIN שטוחים |
CASE WHEN … THEN … ELSE … END |
✓ | ישיר (צורות searched ו-simple) |
| GROUP BY משתמע | ✓ | פריטי RETURN לא-מצוברים הופכים למפתחות GROUP BY כאשר לפריט כלשהו יש אגרגט |
הקרנות מפה (Map Projections)¶
| תחביר | מיפוי SQL |
|---|---|
n { .prop1, .prop2 } |
MAP(ARRAY['prop1','prop2'], ARRAY[n."prop1",n."prop2"]) |
n { .* } |
MAP(ARRAY[all props...], ARRAY[n."col",...]) — מורחב מהסכמה |
n { .*, extra: expr } |
כל מאפייני הסכמה בתוספת מפתח בעל-שם; MAP משולב |
n { key: expr } |
MAP(ARRAY['key'], ARRAY[expr]) |
מאפייני אובייקט. עמודה שמחזיקה אובייקט מוצהר (האובייקט המוטמע של טיפוס GraphQL מרוחק, או כל עמודה שנרשמה עם שדות אובייקט) היא מאפיין מסוג map: RETURN n.address מחזיר אותה כ-map, בכל צורה שהמנוע מחזיר אותה, והצומת n נושא אותה בין המאפיינים שלו. שדה GraphQL מרוחק שמצביע על טיפוס רשום, אובייקט אחד או רשימה, אינו מאפיין; מגיעים אליו דרך הקשר שלו.
פונקציות אגרגציה¶
| Cypher | SQL |
|---|---|
count(*), count(x) |
ישיר |
count(DISTINCT x) |
count(DISTINCT x) |
collect(x) |
array_agg(x) |
avg, sum, min, max |
ישיר |
stDev(x) |
stddev_samp(x) |
stDevP(x) |
stddev_pop(x) |
percentileCont(x, p) |
approx_percentile(x, p) |
percentileDisc(x, p) |
approx_percentile(x, p) |
פונקציות מחרוזת¶
| Cypher | SQL |
|---|---|
toLower(x) |
lower(x) |
toUpper(x) |
upper(x) |
ltrim(x), rtrim(x), trim(x) |
ישיר |
replace(x, a, b) |
ישיר |
reverse(x) |
ישיר |
split(x, d) |
ישיר |
left(x, n) |
left(x, n) |
right(x, n) |
right(x, n) |
substring(x, start, len) |
substr(x, start+1, len) (אינדקס 0→1) |
size(string) |
char_length(string) |
size(list) |
cardinality(list) |
פונקציות המרת טיפוס¶
| 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 |
וריאנטים של TRY_CAST |
פונקציות מתמטיות¶
| Cypher | SQL |
|---|---|
log(x) |
ln(x) (לוג טבעי) |
log2(x) |
log2(x) |
range(start, end) |
sequence(start, end) |
abs, sqrt, ceil, floor, round, sign |
מועברים כמו-שהם |
פונקציות רשימה¶
| 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 Comprehensions¶
| תחביר | מיפוי 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 Comprehensions¶
| תחביר | מיפוי SQL |
|---|---|
[(a)-[:R]->(b) \| b.prop] |
ARRAY(SELECT b."prop" FROM ... WHERE a.fk = b.pk) |
[(a)-[]->(b:Label) \| b.prop] |
טיפוס-מוסק מהמודל; אותה צורת תת-שאילתת ARRAY |
תתי-שאילתות CALL מתואמות (Correlated)¶
CALL { WITH x MATCH (x)-[:R]->(n) RETURN n.prop AS alias } מתורגם ל-CROSS JOIN LATERAL (SELECT n."prop" AS alias FROM ... WHERE x."pk" = n."fk"). כללים:
- המשתנה מהיקף-חיצוני (
x) חייב להופיע ב-WITH - משתנים מיובאים מרובים (
WITH a, b) נתמכים - הקשר הראשון ב-MATCH הפנימי שמקורו הוא משתנה lateral-bound קובע את ה-
FROMהפנימי ותנאי ה-join - בלוקי
CALL { ... }ברמה-עליונה שאינם-מתואמים (ללאWITH) מטופלים על ידיcypher_calls_to_sql_list
כתיבות (Writes)¶
Cypher תומכת בשלושה דפוסי כתיבה דרך נקודת הקצה /data/cypher, המבוצעים על ידי 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 … |
שמות מאפיינים ממופים לעמודות דרך הסרת קידומת-דומיין ופתרון alias; ערכים סקלריים של Cypher נכפים (coerced) לטיפוס העמודה היעד. גוף התגובה נושא ספירת affected_rows.
המאפיין של צומת הוא השם המפורסם של העמודה שלו: הכינוי של העמודה כשיש לה כזה, אחרת השם לפי מוסכמת השמות – השם שבו GraphQL ו-SQL קוראים אותה – ב-MATCH, RETURN, CREATE, SET ו-MERGE באותו אופן.
כללים:
- התווית חייבת להיפתר בדיוק לטבלה רשומה אחת. תוויות דו-משמעיות או לא-ידועות הן שגיאות קשות; ללא התאמה מטושטשת (fuzzy). תוויות או טיפוסים חדשים לא ניתנים ליצירה דרך Cypher.
- כל כתיבה נשערת (gated) על הרשאות הכתיבה של הטבלה היעד: התפקיד זקוק לזכות
writeולהרשאה לפעולה, והעמודות הנכתבות חייבות להופיע בהרשאה. תפקיד שאין לו אותן נדחה בזמן קומפילציה. - מחבר המקור התומך חייב לתמוך ב-DML. מקורות read-only (פדרטיביים-Trino, Iceberg ללא מחבר Delta) דוחים כתיבות בזמן תרגום.
- קשרים אינם ניתנים לכתיבה: הם נגזרים מה-joins המוצהרים במודל, לא קצוות מאוחסנים. כוונון (targeting) קשר הוא שגיאה קשה. קשת מגובת junction אינה יוצאת דופן: טבלת השיוך שמאחוריה היא עצמה טבלה רשומה, והשורות נכתבות לטבלה הזו, לא לקשת.
- כתיבות רצות דרך צינור הכתיבה המלא: הזרקת RLS ו-hooks לאחר-מוטציה (ביטול תוקף מטמון-תגובה, סימון-מיושן של materialized-view, אירועי שינוי Kafka, טעינה מחדש של טבלה חמה).
MERGE,DETACH DELETE, ו-REMOVEאינם נתמכים ונדחים בזמן פענוח.
גישת פרוטוקול¶
Cypher מגיעה לאותו צינור מנוהל על פני שתי תעבורות:
- HTTP —
POST /data/cypherעם גוף JSON ({"query": "...", "params": {...}}). מחזיר שורות מוקלדות, אוaffected_rowsעבור כתיבות. משתני גרף בסעיףRETURNמסודרים כ-JSON: nodes נושאיםid,label,tableLabel, ו-properties; edges נושאיםidentity,start,end,type,properties,startNode, ו-endNode; paths נושאיםnodes,edges, ו-length/hops. commands רשומים ניתנים לקריאה גם כאן דרךCALL fn(args) YIELD col1, col2, וארגומנטים פוזיציוניים ממופים לשמות הארגומנטים המוצהרים של ה-command לפי סדר. - Bolt — שרת פרוטוקול בינארי תואם-Neo4j (קודק PackStream, framing מפוצל-chunk) המאפשר ל-Neo4j Browser, Bloom, ולדרייברי Bolt להריץ Cypher על פני הגרף הפדרטיבי. הוא מתחיל כאשר
PROVISA_BOLT_PORTמוגדר לערך שאינו-אפס וכבוי כברירת מחדל; הגדירוPROVISA_BOLT_CERT/PROVISA_BOLT_KEYעבור TLS. אימות Bolt ממפה principal למשתמש ומסד-נתונים לתפקיד:SHOW DATABASESמציג רשומה אחת לכל זוג (תצוגה × תפקיד), בשםprovisa_<role>(דומייני עסקים) אוprovisa_ops_<role>(עם דומייני system/meta/ops);:useבוחר את התפקיד והתצוגה הפעילים. קשרים מקבלים מזהי integer מתמידים דרך טבלתrel_ids, המשקפת את עיצובnode_ids. commands רשומים ניתנים לקריאה עםCALL command(args), וארגומנטים פוזיציוניים ממופים לשמות הארגומנטים המוצהרים לפי סדר; פרוצדורותCALL dbms.*/CALL db.*מקבלות עדיפות.
אנליטיקת גרף¶
POST /data/graph-analytics מריצה שאילתת Cypher, בונה גרף NetworkX בזיכרון מה-nodes וה-edges המתקבלים, מריצה אלגוריתם בעל-שם, וממזגת מילון _analytics לתוך כל node ו-edge לפני החזרתם כ-JSON עם שדה elapsed_ms. מפתחות ה-_analytics משתנים לפי אלגוריתם: centrality מניב score; זיהוי קהילה מניב cluster; k-core מניב core_number; degree centrality מוסיף in_degree ו-out_degree. נקודת הקצה דוחה גרפים מעל גודל הניתן-לתצורה (ברירת מחדל 10,000 nodes / 50,000 edges) עם HTTP 413; Girvan-Newman מוגבל ל-500 nodes אלא אם הקורא מעביר force=true.
מגבלות¶
אילוצי עיצוב¶
-
כתיבות מוגבלות ל-
CREATE,SET, ו-DELETE. אלה מבוצעות ככתיבות טבלה ישירות דרך אותו צינור כמו מוטציות GraphQL ו-SQL. ראו §כתיבות למעלה.MERGE,DETACH DELETE, ו-REMOVEנדחים בזמן פענוח. פרוצדורות APOC נדחות אף הן. -
מאפייני קשר קיימים רק בקשתות מגובות junction. קשת שמוצהרת על זוג עמודות של מפתח זר קיימת רק כמטא-נתוני צירוף במודל ואינה שומרת תכונות, ולכן
WHERE r.since > 2020אוRETURN r.weightחסרי משמעות עליה. קשת שמוצהרת על טבלת צומת כן נושאת אותן. שאר העמודות של טבלת הקישור הן מאפייני הקשר, ו-RETURN rמחזיר אותן.WHEREעל אחת מהן מהודר לפרדיקט על ה-alias של הצומת, ולכן הוא מגביל את המעבר עצמו במקום לסנן שורות אחרי ההרכבה. טבלת הצומת מוסרת מצד הצמתים של סכמת הגרף: כאן היא קשת, ובכל מקום אחר טבלה. -
מעבר דו-כיווני
(a)-[]-(b)נכתב מחדש כ-UNION ALL קדימה ואחורה של כל קשר מכוון תואם במודל. לכל קשר במודל יש כיוון, והתחביר הדו-כיווני הוא קיצור שמתרחב לשני הכיוונים. הענפים הנוספים נפלטים ברמה החיצונית ביותר של השאילתה. תבנית MATCH מאוחרת יותר באותה שאילתה אינה משוכפלת בין הענפים, וזה מגביל תבניות דו-כיווניות בשאילתה עם כמה פסוקיות MATCH. -
נתיבים רקורסיביים דורשים גבול. דפוסי אורך-משתנה (
[*]) חייבים לכלול גבול עליון (למשל[*..10]). מעבר לא-חסום נדחה בזמן פענוח כדי למנוע CTE-ים רקורסיביים חסרי-שליטה.
הערות התנהגות¶
-
shortestPathעל נתיבים שאינם-self-referential משתמש ב-JOIN שטוח בלי סידור hops. כשטיפוסי ההתחלה והסיום שונים ובסכמה אין קשר self-referential, המתרגם פולט שרשרת JOIN שטוחה לאורך נתיב הסכמה הקצר ביותר. נתיב הקוד הזה לא עוקב אחרי hops, ולכן הוא לא פולטORDER BY hops. התוצאה היא נתיב הסכמה הקצר ביותר מבחינה מבנית. הוא עשוי להיות שונה מהנתיב הקצר ביותר דרך הנתונים על פני כמה שורות. -
נתיבי סכמה מרובים מייצרים
UNION ALL. כאשר שני נתיבי סכמה בעלי אותה ספירת-hop מחברים את אותם טיפוסי התחלה וסיום (למשלPerson -[WORKS_AT]-> Companyו-Person -[MANAGES]-> Company), שניהם נפלטים כענפיUNION ALL. הסרת כפילויות של שורות המופיעות בשני הענפים אינה מבוצעת. -
RelationshipMappingאחד לכל צירוף מקור→יעד וצירוף rel_type. אם שני שדות GraphQL על אותו טיפוס מקור מייצרים את אותה מחרוזתrel_type(לאחר uppercasing) לאותו טיפוס יעד, הרישום השני דורס את הראשון ב-CypherLabelMap.relationships. מפתח הקשר כולל שמות טיפוס מקור ויעד, כך שצירופי מקור/יעד שונים עם אותו שם טיפוס מקבלים כל אחד רשומה משלו ואינם מושפעים. -
CTE-ים בסעיף
WITHנקראים_w0,_w1, … שמות מוקצים פוזיציונית בתוך קריאת תרגום יחידה. הרכבת מספר שאילתות מתורגמות (למשל ב-batch) יכולה לייצר שמות CTE מתנגשים אם הן משורשרות באופן נאיבי.
כיסוי ביטויים ודפוסים¶
ביטויי Cypher מפוענחים ל-AST ומורדים node-ל-node ל-SQL (provisa/cypher/expr_parser.py, provisa/cypher/expr_visitor.py). הדקדוק עוקב אחר מגדל העדיפויות oC_Expression של openCypher. נתמך: ליטרלים, פרמטרים, גישת מאפיין, n.prop, אינדקס ו-slice, אריתמטיקה (+ - * / % ^), השוואה, IN, STARTS WITH / ENDS WITH / CONTAINS / =~, IS [NOT] NULL, בוליאני AND / OR / XOR / NOT, CASE, ליטרלי רשימה ומפה, comprehensions של רשימה ודפוס (כולל קשירת נתיב p = (…)), הקרנת מפה, reduce, המכמתים (quantifiers) all / any / none / single, תתי-שאילתות אקזיסטנציאליות, וקריאות פונקציה.
-
תוויות קבועות; אינכם יכולים ליצור טיפוסי אובייקט דרך Cypher. תווית נפתרת לדומיין מוכר, לטיפוס אובייקט מוכר או ל-
domain:object_typeמוסמך, הקבוצה הסגורה שהסכמה הרשומה מגדירה. Cypher לעולם לא מוסיף תווית או טיפוס חדשים. אפשר ליצור מופעים רק של טיפוסים שכבר מוגדרים במקור נתונים שניתן לכתוב אליו:CREATEכותב שורות לטבלה כזו (ראו §Writes) ואינו יכול להגדיר תווית או טיפוס חדשים. שתי צורות התווית מתקבלות ומשמעותן אותה בדיקה, הצורה הסופיתn:Labelוהצורה המפורטתn IS :Label(עם השלילהn IS NOT :Label). תווית מוסמכת נכתבתn:domain:object_type. -
shortestPathו-allShortestPathsפועלים רק בתוךMATCH. בתבנית (MATCH p = shortestPath((a:Person)-[:KNOWS*..5]->(b:Person))) הם מתורגמים ל-CTE מסוגWITH RECURSIVEודורשים צומתי מקור ויעד עם תוויות. הם אינם נתמכים במיקום של ביטוי, לדוגמהRETURN shortestPath((a)-[*]->(b))אוWHERE length(shortestPath((a)-[*]->(b))) < 5. הכתיבה הרקורסיבית מחדש מונעת מפסוקית ה-MATCH, ואין תת-שאילתה מתואמת שתישא אותה. -
List comprehensions,
REDUCE, ומכמתים פועלים על ערכי רשימה; pattern comprehensions חוצות (traverse).reduce(...),all/any/none/single(...)וה-list comprehension[x IN list | …]פועלים על ביטוי רשימה ומתורגמים לפונקציות הרשימה מסדר גבוה של המנוע. הם עצמם לא עוברים על הגרף. ה-pattern comprehension[(a)-[:R]->(b) WHERE p | e]כן חוצה: תבנית הגרף שלה מטופלת כתת-שאילתה מתואמת, ולכן המקור שלה הוא מעבר. כדי להזין תוצאות מעבר לצורות הרשימה, השתמשו ב-nodes(p)/relationships(p)/collect(...), או ב-pattern comprehension ישירות.