תמיכה בשאילתות Cypher¶
Provisa מתרגמת תת-קבוצה של openCypher ל-SQL דרך המודול provisa/cypher/. (REQ-345, REQ-347) שאילתות מפוענחות על ידי parser recursive-descent מותאם אישית (ללא ספריית Cypher חיצונית) (REQ-571), נפתרות-סכמה (schema-resolved) מול השכבה הסמנטית (REQ-351), ונפלטות כ-SQL, ואז מנותבות למנוע הביצוע היעד. (REQ-066, REQ-067, REQ-347)
תכונות ממומשות¶
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 (REQ-572) |
CALL db.relationshipTypes() |
✓ | מחזיר סוגי קשר מהשכבה הסמנטית (REQ-572) |
CALL db.propertyKeys() |
✓ | מחזיר את כל שמות מפתחות המאפיינים על פני כל סוגי ה-node (REQ-572) |
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]) |
פונקציות אגרגציה¶
| 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"). (REQ-573) כללים:
- המשתנה מהיקף-חיצוני (
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. (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 |
שמות מאפיינים ממופים לעמודות דרך הסרת קידומת-דומיין ופתרון alias; ערכים סקלריים של Cypher נכפים (coerced) לטיפוס העמודה היעד. (REQ-666, REQ-668) גוף התגובה נושא ספירת affected_rows. (REQ-670)
כללים:
- התווית חייבת להיפתר בדיוק לטבלה רשומה אחת. תוויות דו-משמעיות או לא-ידועות הן שגיאות קשות; ללא התאמה מטושטשת (fuzzy). (REQ-661) תוויות או טיפוסים חדשים לא ניתנים ליצירה דרך Cypher. (REQ-662)
- כל כתיבה נשערת (gated) על ACL
writable_byשל הטבלה היעד; תפקיד ללא זכויות כתיבה נדחה בזמן קומפילציה. (REQ-663) - מחבר המקור התומך חייב לתמוך ב-DML. מקורות read-only (פדרטיביים-Trino, Iceberg ללא מחבר Delta) דוחים כתיבות בזמן תרגום. (REQ-664)
- קשרים אינם ניתנים לכתיבה — הם נגזרים מ-joins של מפתח-זר, לא קצוות מאוחסנים. כוונון (targeting) קשר הוא שגיאה קשה. (REQ-665)
- כתיבות רצות דרך צינור הכתיבה המלא: הזרקת RLS ו-hooks לאחר-מוטציה (ביטול תוקף מטמון-תגובה, סימון-מיושן של materialized-view, אירועי שינוי Kafka, טעינה מחדש של טבלה חמה). (REQ-798)
MERGE,DETACH DELETE, ו-REMOVEאינם נתמכים ונדחים בזמן פענוח. (REQ-671)
גישת פרוטוקול¶
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. (REQ-750) commands רשומים ניתנים לקריאה גם כאן דרךCALL fn(args) YIELD col1, col2— ארגומנטים פוזיציוניים ממופים לשמות הארגומנטים המוצהרים של ה-command לפי סדר. (REQ-1156) [tool-verified:provisa/api/rest/registered_call.py:113-143] - Bolt — שרת פרוטוקול בינארי תואם-Neo4j (קודק PackStream, framing מפוצל-chunk) המאפשר ל-Neo4j Browser, Bloom, ולדרייברי Bolt להריץ Cypher על פני הגרף הפדרטיבי. (REQ-802) הוא מתחיל כאשר
PROVISA_BOLT_PORTמוגדר לערך שאינו-אפס וכבוי כברירת מחדל; הגדירוPROVISA_BOLT_CERT/PROVISA_BOLT_KEYעבור TLS. [tool-verified:provisa/api/app_startup.py:317-338] אימות Bolt ממפה principal למשתמש ומסד-נתונים לתפקיד:SHOW DATABASESמציג רשומה אחת לכל זוג (תצוגה × תפקיד), בשםprovisa_<role>(דומייני עסקים) אוprovisa_ops_<role>(עם דומייני system/meta/ops);:useבוחר את התפקיד והתצוגה הפעילים. (REQ-807) קשרים מקבלים מזהי integer מתמידים דרך טבלתrel_ids, המשקפת את עיצובnode_ids. (REQ-806) commands רשומים ניתנים לקריאה עםCALL command(args)— ארגומנטים פוזיציוניים ממופים לשמות הארגומנטים המוצהרים לפי סדר; פרוצדורותCALL dbms.*/CALL db.*מקבלות עדיפות. (REQ-1156) [tool-verified:provisa/bolt/session.py:722-749]
אנליטיקת גרף¶
POST /data/graph-analytics מריצה שאילתת Cypher, בונה גרף NetworkX בזיכרון מה-nodes וה-edges המתקבלים, מריצה אלגוריתם בעל-שם, וממזגת מילון _analytics לתוך כל node ו-edge לפני החזרתם כ-JSON עם שדה elapsed_ms. (REQ-642) מפתחות ה-_analytics משתנים לפי אלגוריתם: centrality מניב score; זיהוי קהילה מניב cluster; k-core מניב core_number; degree centrality מוסיף in_degree ו-out_degree. (REQ-643) נקודת הקצה דוחה גרפים מעל גודל הניתן-לתצורה (ברירת מחדל 10,000 nodes / 50,000 edges) עם HTTP 413; Girvan-Newman מוגבל ל-500 nodes אלא אם הקורא מעביר force=true. (REQ-650, REQ-651)
מגבלות¶
אילוצי עיצוב¶
-
כתיבות מוגבלות ל-
CREATE,SET, ו-DELETE. אלה מבוצעות ככתיבות טבלה ישירות דרך אותו צינור כמו מוטציות GraphQL ו-SQL. (REQ-818, REQ-666, REQ-667, REQ-668) ראו §כתיבות למעלה.MERGE,DETACH DELETE, ו-REMOVEנדחים בזמן פענוח. (REQ-671, REQ-818) פרוצדורות APOC נדחות אף הן. -
ללא מאפייני קשר. קשרים (
-[r:TYPE]->) קיימים אך ורק כמטא-דאטת join בשכבה הסמנטית. (REQ-574) הם אינם נושאים תכונות מאוחסנות, כך של-WHERE r.since > 2020אוRETURN r.weightאין משמעות ואינם נתמכים. -
מעבר דו-כיווני
(a)-[]-(b)נכתב מחדש ל-UNION ALL קדימה+אחורה של כל הקשרים המכוונים התואמים מהשכבה הסמנטית. (REQ-575) כל קשר בשכבה הסמנטית הוא כיווני; תחביר דו-כיווני הוא סוכר תחבירי המתרחב לשני הכיוונים. ענפים נוספים נפלטים ברמת השאילתה החיצונית-ביותר — דפוסי MATCH נוספים באותה שאילתה אינם משוכפלים על פני ענפים (מגבלה עבור דו-כיווני מרובה-MATCH). -
נתיבים רקורסיביים דורשים גבול. דפוסי אורך-משתנה (
[*]) חייבים לכלול גבול עליון (למשל[*..10]). (REQ-348) מעבר לא-חסום נדחה בזמן פענוח כדי למנוע CTE-ים רקורסיביים חסרי-שליטה.
הערות התנהגות¶
-
shortestPathעל נתיבים שאינם-self-referential משתמש ב-JOIN שטוח, לא בסידור hops. כאשר טיפוסי ההתחלה והסיום שונים ולא קיים קשר self-referential בסכמה, המתרגם פולט שרשרת JOIN שטוחה (נתיב הסכמה הקצר-ביותר). (REQ-576) הוא אינו פולטORDER BY hopsכי hops אינם נעקבים באותו נתיב קוד. התוצאה היא נתיב הסכמה הקצר-ביותר מבחינה מבנית, לא הנתיב הקצר-ביותר-בנתונים על פני מספר שורות. -
נתיבי סכמה מרובים מייצרים
UNION ALL. כאשר שני נתיבי סכמה בעלי אותה ספירת-hop מחברים את אותם טיפוסי התחלה וסיום (למשלPerson -[WORKS_AT]-> Companyו-Person -[MANAGES]-> Company), שניהם נפלטים כענפיUNION ALL. (REQ-577) הסרת כפילויות של שורות המופיעות בשני הענפים אינה מבוצעת. -
RelationshipMappingאחד לכל צירוף מקור→יעד וצירוף rel_type. אם שני שדות GraphQL על אותו טיפוס מקור מייצרים את אותה מחרוזתrel_type(לאחר uppercasing) לאותו טיפוס יעד, הרישום השני דורס את הראשון ב-CypherLabelMap.relationships. מפתח הקשר כולל שמות טיפוס מקור ויעד, כך שצירופי מקור/יעד שונים עם אותו שם טיפוס מקבלים כל אחד רשומה משלו ואינם מושפעים. -
CTE-ים בסעיף
WITHנקראים_w0,_w1, … (REQ-578) שמות מוקצים פוזיציונית בתוך קריאת תרגום יחידה. הרכבת מספר שאילתות מתורגמות (למשל ב-batch) יכולה לייצר שמות CTE מתנגשים אם הן משורשרות באופן נאיבי.
כיסוי ביטויים ודפוסים (REQ-913)¶
ביטויי 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כותב שורות לתוך טבלה כזו (ראו §כתיבות) אך אינו יכול להגדיר תווית או טיפוס חדשים. (REQ-662) שתי צורות התווית מתקבלות ופירושן אותו מבחן: הפוסטפיקס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))) הם מתורגמים ל-CTEWITH RECURSIVEודורשים nodes מקור ויעד מתויגים. בשימוש בעמדת ביטוי — למשל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]כן חוצה: דפוס הגרף שלו מטופל כתת-שאילתה מתואמת, כך שזהו comprehension שמקורו הוא מעבר (traversal). הזינו תוצאות מעבר לתוך צורות הרשימה עםnodes(p)/relationships(p)/collect(...), או השתמשו ב-pattern comprehension ישירות.