Skip to main content

ursa_sql/
property_graph.rs

1//! The table clauses of `CREATE PROPERTY GRAPH`: SQL/PGQ's element tables
2//! (#295).
3//!
4//! ```text
5//! CREATE [OR REPLACE] PROPERTY GRAPH [IF NOT EXISTS] name
6//!   [VERTEX TABLES ( vertex_table [, …] )]
7//!   [EDGE TABLES ( edge_table [, …] )]
8//!
9//! vertex_table := table [AS name] [KEY (column)] [labels]
10//! edge_table   := table [AS name] [KEY (column)]
11//!                 SOURCE KEY (column) [REFERENCES vertex (column)]
12//!                 DESTINATION KEY (column) [REFERENCES vertex (column)]
13//!                 [labels]
14//! labels       := properties                       -- the default label
15//!               | DEFAULT LABEL [properties]
16//!               | LABEL name [properties] [LABEL name [properties] …]
17//!               | properties LABEL name [LABEL name …]   (DuckPGQ's order)
18//! properties   := PROPERTIES (column [AS name], …)
19//!               | PROPERTIES [ARE] ALL COLUMNS [EXCEPT (column, …)]
20//!               | NO PROPERTIES
21//! ```
22//!
23//! At least one of the two clauses is needed. Ursa's relaxation stays: a graph
24//! may have no vertex tables, and then its nodes are its edges' endpoints. A
25//! vertex `KEY` may be left out: the engine takes the table's primary key, or
26//! the column the edges' `REFERENCES` name, as DuckPGQ does. `REFERENCES`
27//! names a vertex table by its element name (its `AS` name, or else the table's
28//! name). Still refused, with a clear error: multi-column keys, `SOURCE vertex`
29//! without `KEY`, and property expressions other than a column. `NODE TABLES`
30//! and `RELATIONSHIP TABLES`, SQL/PGQ's synonyms, are accepted.
31
32use std::fmt;
33
34use datafusion::error::{DataFusionError, Result};
35use datafusion::sql::sqlparser::ast::{Ident, ObjectName};
36use datafusion::sql::sqlparser::parser::Parser;
37use datafusion::sql::sqlparser::tokenizer::Token;
38
39/// One entry of `VERTEX TABLES (…)`.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct VertexTableDef {
42    pub table: ObjectName,
43    /// `AS name`.
44    pub alias: Option<Ident>,
45    /// `KEY (column)`; `None` when left out.
46    pub key: Option<Ident>,
47    pub labels: Vec<LabelDef>,
48}
49
50/// One entry of `EDGE TABLES (…)`.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct EdgeTableDef {
53    pub table: ObjectName,
54    pub alias: Option<Ident>,
55    /// The edge table's own `KEY (column)`, if given.
56    pub key: Option<Ident>,
57    pub source: EndpointDef,
58    pub destination: EndpointDef,
59    pub labels: Vec<LabelDef>,
60}
61
62/// `KEY (key) [REFERENCES table (column)]`, after `SOURCE` or `DESTINATION`.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct EndpointDef {
65    pub key: Ident,
66    pub references: Option<References>,
67}
68
69/// `REFERENCES vertex (column)`: a vertex table's element name or table name.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct References {
72    pub table: ObjectName,
73    pub column: Ident,
74}
75
76/// One label of an element table and its properties. `name: None` is the
77/// default label (written `DEFAULT LABEL`, or implied by a bare `PROPERTIES`
78/// clause), which is the element table's name.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct LabelDef {
81    pub name: Option<Ident>,
82    pub properties: PropertiesDef,
83}
84
85/// A label's properties.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum PropertiesDef {
88    /// No properties clause: every column, as `ALL COLUMNS`.
89    Default,
90    /// `PROPERTIES [ARE] ALL COLUMNS [EXCEPT (…)]`.
91    AllColumns { except: Vec<Ident> },
92    /// `PROPERTIES (column [AS name], …)`.
93    List(Vec<(Ident, Option<Ident>)>),
94    /// `NO PROPERTIES`.
95    None,
96}
97
98fn comma_list<T: fmt::Display>(items: &[T]) -> String {
99    items
100        .iter()
101        .map(ToString::to_string)
102        .collect::<Vec<_>>()
103        .join(", ")
104}
105
106fn write_labels(f: &mut fmt::Formatter<'_>, labels: &[LabelDef]) -> fmt::Result {
107    for label in labels {
108        match &label.name {
109            Some(name) => write!(f, " LABEL {name}")?,
110            None if label.properties == PropertiesDef::Default => write!(f, " DEFAULT LABEL")?,
111            None => {}
112        }
113        write!(f, "{}", label.properties)?;
114    }
115    Ok(())
116}
117
118impl fmt::Display for PropertiesDef {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match self {
121            PropertiesDef::Default => Ok(()),
122            PropertiesDef::AllColumns { except } if except.is_empty() => {
123                write!(f, " PROPERTIES ARE ALL COLUMNS")
124            }
125            PropertiesDef::AllColumns { except } => {
126                write!(
127                    f,
128                    " PROPERTIES ARE ALL COLUMNS EXCEPT ({})",
129                    comma_list(except)
130                )
131            }
132            PropertiesDef::List(columns) => {
133                let items: Vec<String> = columns
134                    .iter()
135                    .map(|(column, alias)| match alias {
136                        Some(alias) => format!("{column} AS {alias}"),
137                        None => column.to_string(),
138                    })
139                    .collect();
140                write!(f, " PROPERTIES ({})", items.join(", "))
141            }
142            PropertiesDef::None => write!(f, " NO PROPERTIES"),
143        }
144    }
145}
146
147impl fmt::Display for VertexTableDef {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "{}", self.table)?;
150        if let Some(alias) = &self.alias {
151            write!(f, " AS {alias}")?;
152        }
153        if let Some(key) = &self.key {
154            write!(f, " KEY ({key})")?;
155        }
156        write_labels(f, &self.labels)
157    }
158}
159
160impl fmt::Display for EdgeTableDef {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        write!(f, "{}", self.table)?;
163        if let Some(alias) = &self.alias {
164            write!(f, " AS {alias}")?;
165        }
166        if let Some(key) = &self.key {
167            write!(f, " KEY ({key})")?;
168        }
169        write!(
170            f,
171            " SOURCE {} DESTINATION {}",
172            self.source, self.destination
173        )?;
174        write_labels(f, &self.labels)
175    }
176}
177
178impl fmt::Display for EndpointDef {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        write!(f, "KEY ({})", self.key)?;
181        if let Some(r) = &self.references {
182            write!(f, " REFERENCES {} ({})", r.table, r.column)?;
183        }
184        Ok(())
185    }
186}
187
188/// `VERTEX TABLES (…)` and `EDGE TABLES (…)`, as written after the graph's name.
189pub(crate) fn write_tables(
190    f: &mut fmt::Formatter<'_>,
191    vertices: &[VertexTableDef],
192    edges: &[EdgeTableDef],
193) -> fmt::Result {
194    if !vertices.is_empty() {
195        write!(f, " VERTEX TABLES ({})", comma_list(vertices))?;
196    }
197    if !edges.is_empty() {
198        write!(f, " EDGE TABLES ({})", comma_list(edges))?;
199    }
200    Ok(())
201}
202
203/// What a feature beyond the supported grammar is refused with.
204const NOT_SUPPORTED: &str = "not supported yet";
205
206/// Parse the table clauses, after the graph's name.
207pub(crate) fn parse_tables(p: &mut Parser) -> Result<(Vec<VertexTableDef>, Vec<EdgeTableDef>)> {
208    let vertices =
209        if consume_words(p, &["VERTEX", "TABLES"]) || consume_words(p, &["NODE", "TABLES"]) {
210            parse_list(p, parse_vertex_table)?
211        } else {
212            Vec::new()
213        };
214    let edges =
215        if consume_words(p, &["EDGE", "TABLES"]) || consume_words(p, &["RELATIONSHIP", "TABLES"]) {
216            parse_list(p, parse_edge_table)?
217        } else if vertices.is_empty() {
218            return Ok(p.expected_ref("VERTEX TABLES or EDGE TABLES", p.peek_token_ref())?);
219        } else {
220            Vec::new()
221        };
222    if word(p, 0, "VERTEX") || word(p, 0, "NODE") {
223        return unsupported(
224            p,
225            "VERTEX TABLES after EDGE TABLES is",
226            "write VERTEX TABLES first",
227        );
228    }
229    if word(p, 0, "EDGE") || word(p, 0, "RELATIONSHIP") {
230        return unsupported(
231            p,
232            "a second EDGE TABLES clause is",
233            "list every edge table in one EDGE TABLES (…)",
234        );
235    }
236    Ok((vertices, edges))
237}
238
239/// `( item [, item …] )`.
240fn parse_list<T>(p: &mut Parser, item: fn(&mut Parser) -> Result<T>) -> Result<Vec<T>> {
241    p.expect_token(&Token::LParen)?;
242    let mut items = vec![item(p)?];
243    while p.consume_token(&Token::Comma) {
244        items.push(item(p)?);
245    }
246    p.expect_token(&Token::RParen)?;
247    Ok(items)
248}
249
250/// `table [AS name]`.
251fn parse_element_name(p: &mut Parser) -> Result<(ObjectName, Option<Ident>)> {
252    let table = p.parse_object_name(false)?;
253    let alias = if consume_words(p, &["AS"]) {
254        Some(p.parse_identifier()?)
255    } else {
256        None
257    };
258    Ok((table, alias))
259}
260
261fn parse_vertex_table(p: &mut Parser) -> Result<VertexTableDef> {
262    let (table, alias) = parse_element_name(p)?;
263    let key = if consume_words(p, &["KEY"]) {
264        Some(parse_key(p)?)
265    } else {
266        None
267    };
268    let labels = parse_labels(p)?;
269    if !matches!(p.peek_token_ref().token, Token::RParen | Token::Comma) {
270        return Ok(p.expected_ref("KEY, LABEL, PROPERTIES, ',' or ')'", p.peek_token_ref())?);
271    }
272    Ok(VertexTableDef {
273        table,
274        alias,
275        key,
276        labels,
277    })
278}
279
280fn parse_edge_table(p: &mut Parser) -> Result<EdgeTableDef> {
281    let (table, alias) = parse_element_name(p)?;
282    let key = if consume_words(p, &["KEY"]) {
283        Some(parse_key(p)?)
284    } else {
285        None
286    };
287    expect_word(p, "SOURCE")?;
288    let source = parse_endpoint(p, "SOURCE")?;
289    expect_word(p, "DESTINATION")?;
290    let destination = parse_endpoint(p, "DESTINATION")?;
291    let labels = parse_labels(p)?;
292    Ok(EdgeTableDef {
293        table,
294        alias,
295        key,
296        source,
297        destination,
298        labels,
299    })
300}
301
302fn parse_endpoint(p: &mut Parser, side: &str) -> Result<EndpointDef> {
303    if !consume_words(p, &["KEY"]) {
304        if matches!(p.peek_token_ref().token, Token::Word(_)) {
305            return unsupported(
306                p,
307                &format!("{side} without KEY is"),
308                &format!("name the column with {side} KEY (column) REFERENCES vertex_table (key)"),
309            );
310        }
311        return Ok(p.expected_ref("KEY", p.peek_token_ref())?);
312    }
313    let key = parse_key(p)?;
314    let references = if consume_words(p, &["REFERENCES"]) {
315        let table = p.parse_object_name(false)?;
316        let column = parse_key(p)?;
317        Some(References { table, column })
318    } else {
319        None
320    };
321    Ok(EndpointDef { key, references })
322}
323
324/// The label and property clauses of one element table.
325fn parse_labels(p: &mut Parser) -> Result<Vec<LabelDef>> {
326    if consume_words(p, &["DEFAULT", "LABEL"]) {
327        let properties = parse_properties(p)?;
328        return Ok(vec![LabelDef {
329            name: None,
330            properties,
331        }]);
332    }
333    // DuckPGQ writes the properties first: `PROPERTIES (id, name) LABEL
334    // person` gives `person` those properties (#307). A label with its own
335    // properties clause keeps it.
336    let leading = parse_properties(p)?;
337    let mut labels = Vec::new();
338    while consume_words(p, &["LABEL"]) {
339        let name = p.parse_identifier()?;
340        let properties = match parse_properties(p)? {
341            PropertiesDef::Default => leading.clone(),
342            own => own,
343        };
344        labels.push(LabelDef {
345            name: Some(name),
346            properties,
347        });
348    }
349    if labels.is_empty() && leading != PropertiesDef::Default {
350        labels.push(LabelDef {
351            name: None,
352            properties: leading,
353        });
354    }
355    if word(p, 0, "LABELS") || word(p, 0, "DEFAULT") {
356        return unsupported(
357            p,
358            "this label clause is",
359            "write LABEL name, once per label",
360        );
361    }
362    Ok(labels)
363}
364
365/// A properties clause, or `Default` when there is none.
366fn parse_properties(p: &mut Parser) -> Result<PropertiesDef> {
367    if consume_words(p, &["NO", "PROPERTIES"]) {
368        return Ok(PropertiesDef::None);
369    }
370    if !consume_words(p, &["PROPERTIES"]) {
371        return Ok(PropertiesDef::Default);
372    }
373    if consume_words(p, &["ARE", "ALL", "COLUMNS"]) || consume_words(p, &["ALL", "COLUMNS"]) {
374        let except = if consume_words(p, &["EXCEPT"]) {
375            parse_list(p, |p| Ok(p.parse_identifier()?))?
376        } else {
377            Vec::new()
378        };
379        return Ok(PropertiesDef::AllColumns { except });
380    }
381    let columns = parse_list(p, |p| {
382        let column = match &p.peek_token_ref().token {
383            Token::Word(_) => p.parse_identifier()?,
384            _ => {
385                return unsupported(
386                    p,
387                    "a property that is not a column is",
388                    "compute it in a view, and list the view's column",
389                );
390            }
391        };
392        if !matches!(
393            p.peek_token_ref().token,
394            Token::Comma | Token::RParen | Token::Word(_)
395        ) {
396            return unsupported(
397                p,
398                "a property that is not a column is",
399                "compute it in a view, and list the view's column",
400            );
401        }
402        let alias = if consume_words(p, &["AS"]) {
403            Some(p.parse_identifier()?)
404        } else {
405            None
406        };
407        Ok((column, alias))
408    })?;
409    Ok(PropertiesDef::List(columns))
410}
411
412/// `(column)`: a key of exactly one column.
413fn parse_key(p: &mut Parser) -> Result<Ident> {
414    p.expect_token(&Token::LParen)?;
415    let column = p.parse_identifier()?;
416    if p.peek_token_ref().token == Token::Comma {
417        return unsupported(p, "multi-column keys are", ONE_COLUMN_KEYS);
418    }
419    p.expect_token(&Token::RParen)?;
420    Ok(column)
421}
422
423/// The hint for a key of more than one column.
424const ONE_COLUMN_KEYS: &str = "KEY, SOURCE KEY, DESTINATION KEY and REFERENCES take one column";
425
426/// The error for a SQL/PGQ feature beyond what Ursa supports, at the next
427/// token: "`what` not supported yet at Line: …, Column: …; `hint`". The hint
428/// comes after the main clause, so the sentence reads straight.
429fn unsupported<T>(p: &Parser, what: &str, hint: &str) -> Result<T> {
430    Err(DataFusionError::NotImplemented(format!(
431        "{what} {NOT_SUPPORTED}{}; {hint}",
432        p.peek_token_ref().span.start
433    )))
434}
435
436/// Whether the `n`th token ahead is the unquoted word `word`, in any case.
437pub(crate) fn word(p: &Parser, n: usize, word: &str) -> bool {
438    match &p.peek_nth_token_ref(n).token {
439        Token::Word(w) => w.quote_style.is_none() && w.value.eq_ignore_ascii_case(word),
440        _ => false,
441    }
442}
443
444/// Consume `words` if they are next.
445pub(crate) fn consume_words(p: &mut Parser, words: &[&str]) -> bool {
446    let found = words.iter().enumerate().all(|(i, w)| word(p, i, w));
447    if found {
448        for _ in words {
449            p.next_token();
450        }
451    }
452    found
453}
454
455fn expect_word(p: &mut Parser, expected: &str) -> Result<()> {
456    if consume_words(p, &[expected]) {
457        Ok(())
458    } else {
459        Ok(p.expected_ref(expected, p.peek_token_ref())?)
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use datafusion::sql::sqlparser::ast::ObjectNamePart;
466
467    use super::*;
468    use crate::{parse_sql, CreatePropertyGraph, UrsaStatement};
469
470    fn create(sql: &str) -> CreatePropertyGraph {
471        let mut statements = parse_sql(sql).unwrap_or_else(|e| panic!("{sql}: {e}"));
472        assert_eq!(statements.len(), 1, "{sql}");
473        match statements.pop().unwrap() {
474            UrsaStatement::CreatePropertyGraph(g) => *g,
475            other => panic!("{sql} parsed as {other:?}"),
476        }
477    }
478
479    fn error(sql: &str) -> String {
480        match parse_sql(sql) {
481            Ok(statements) => panic!("{sql} should not parse; got {statements:?}"),
482            Err(e) => ursa_engine::user_message(&e),
483        }
484    }
485
486    fn name(parts: &[&str]) -> ObjectName {
487        ObjectName(
488            parts
489                .iter()
490                .map(|p| ObjectNamePart::Identifier(Ident::new(*p)))
491                .collect(),
492        )
493    }
494
495    fn endpoint(key: &str, references: Option<(&str, &str)>) -> EndpointDef {
496        EndpointDef {
497            key: Ident::new(key),
498            references: references.map(|(table, column)| References {
499                table: name(&[table]),
500                column: Ident::new(column),
501            }),
502        }
503    }
504
505    const GETTING_STARTED: &str = "CREATE PROPERTY GRAPH karate
506  VERTEX TABLES (members KEY (id))
507  EDGE TABLES (
508    ties SOURCE KEY (src) REFERENCES members (id)
509         DESTINATION KEY (dst) REFERENCES members (id)
510  )";
511
512    #[test]
513    fn the_getting_started_graph() {
514        assert_eq!(
515            create(GETTING_STARTED),
516            CreatePropertyGraph {
517                name: name(&["karate"]),
518                or_replace: false,
519                if_not_exists: false,
520                vertices: vec![VertexTableDef {
521                    table: name(&["members"]),
522                    alias: None,
523                    key: Some(Ident::new("id")),
524                    labels: Vec::new(),
525                }],
526                edges: vec![EdgeTableDef {
527                    table: name(&["ties"]),
528                    alias: None,
529                    key: None,
530                    source: endpoint("src", Some(("members", "id"))),
531                    destination: endpoint("dst", Some(("members", "id"))),
532                    labels: Vec::new(),
533                }],
534            }
535        );
536    }
537
538    #[test]
539    fn duckpgqs_graph_without_a_vertex_key() {
540        // DuckPGQ's own test graph: the key is inferred from the REFERENCES.
541        let g = create(
542            "CREATE PROPERTY GRAPH pg
543               VERTEX TABLES (Student)
544               EDGE TABLES (know SOURCE KEY (src) REFERENCES Student (id)
545                                 DESTINATION KEY (dst) REFERENCES Student (id))",
546        );
547        assert_eq!(g.vertices[0].key, None);
548        assert_eq!(g.vertices[0].table, name(&["Student"]));
549        // Vertex tables alone are a graph too.
550        let g = create("CREATE PROPERTY GRAPH v VERTEX TABLES (Student KEY (id))");
551        assert!(g.edges.is_empty());
552    }
553
554    #[test]
555    fn several_tables_aliases_labels_and_properties() {
556        let g = create(
557            "CREATE PROPERTY GRAPH social
558               VERTEX TABLES (
559                 people AS person KEY (id) LABEL person PROPERTIES (id, name AS full_name),
560                 posts KEY (pid) DEFAULT LABEL,
561                 tags NO PROPERTIES
562               )
563               EDGE TABLES (
564                 follows KEY (fid) SOURCE KEY (a) REFERENCES person (id)
565                                   DESTINATION KEY (b) REFERENCES person (id)
566                         LABEL knows LABEL follows PROPERTIES ARE ALL COLUMNS EXCEPT (fid),
567                 wrote SOURCE KEY (author) REFERENCES person (id)
568                       DESTINATION KEY (post) REFERENCES posts (pid)
569                       PROPERTIES ALL COLUMNS
570               )",
571        );
572        assert_eq!(g.vertices.len(), 3);
573        assert_eq!(g.edges.len(), 2);
574        let person = &g.vertices[0];
575        assert_eq!(person.alias, Some(Ident::new("person")));
576        assert_eq!(
577            person.labels,
578            vec![LabelDef {
579                name: Some(Ident::new("person")),
580                properties: PropertiesDef::List(vec![
581                    (Ident::new("id"), None),
582                    (Ident::new("name"), Some(Ident::new("full_name"))),
583                ]),
584            }]
585        );
586        assert_eq!(
587            g.vertices[1].labels,
588            vec![LabelDef {
589                name: None,
590                properties: PropertiesDef::Default
591            }]
592        );
593        assert_eq!(
594            g.vertices[2].labels,
595            vec![LabelDef {
596                name: None,
597                properties: PropertiesDef::None
598            }]
599        );
600        let follows = &g.edges[0];
601        assert_eq!(follows.key, Some(Ident::new("fid")));
602        assert_eq!(follows.labels.len(), 2);
603        assert_eq!(
604            follows.labels[1].properties,
605            PropertiesDef::AllColumns {
606                except: vec![Ident::new("fid")]
607            }
608        );
609        assert_eq!(
610            g.edges[1].labels,
611            vec![LabelDef {
612                name: None,
613                properties: PropertiesDef::AllColumns { except: Vec::new() }
614            }]
615        );
616    }
617
618    #[test]
619    fn accepted_forms() {
620        let g = create(
621            "CREATE PROPERTY GRAPH karate_edges_only
622               EDGE TABLES (ties SOURCE KEY (src) DESTINATION KEY (dst))",
623        );
624        assert!(g.vertices.is_empty());
625        assert_eq!(g.edges[0].source, endpoint("src", None));
626        assert_eq!(g.edges[0].destination, endpoint("dst", None));
627
628        // Keywords in any case; qualified and quoted names; REFERENCES on one side.
629        let g = create(
630            r#"create or replace property graph if not exists "Social"
631                 node tables (db.public."People" key ("Id"))
632                 relationship tables (db.follows source key (a) references db.public."People" ("Id")
633                                                  destination key (b))"#,
634        );
635        assert!(g.or_replace && g.if_not_exists);
636        assert_eq!(g.name.to_string(), r#""Social""#);
637        assert_eq!(g.vertices[0].table.to_string(), r#"db.public."People""#);
638        assert_eq!(g.vertices[0].key, Some(Ident::with_quote('"', "Id")));
639        assert_eq!(g.edges[0].table.to_string(), "db.follows");
640        assert_eq!(
641            g.edges[0]
642                .source
643                .references
644                .as_ref()
645                .unwrap()
646                .table
647                .to_string(),
648            r#"db.public."People""#
649        );
650        assert_eq!(g.edges[0].destination.references, None);
651    }
652
653    #[test]
654    fn display_round_trips() {
655        for sql in [
656            GETTING_STARTED,
657            "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (a) DESTINATION KEY (b))",
658            "CREATE PROPERTY GRAPH v VERTEX TABLES (s)",
659            r#"CREATE OR REPLACE PROPERTY GRAPH IF NOT EXISTS "G" VERTEX TABLES (s.v KEY ("K"))
660               EDGE TABLES (s.e SOURCE KEY (a) DESTINATION KEY (b) REFERENCES s.v ("K"))"#,
661            "CREATE PROPERTY GRAPH g VERTEX TABLES (p AS person KEY (id) LABEL person PROPERTIES \
662             (id, n AS name), q DEFAULT LABEL, r NO PROPERTIES, t PROPERTIES ARE ALL COLUMNS \
663             EXCEPT (x)) EDGE TABLES (e KEY (k) SOURCE KEY (a) REFERENCES person (id) \
664             DESTINATION KEY (b) REFERENCES q (id) LABEL knows LABEL likes NO PROPERTIES)",
665            // DuckPGQ's order: the properties, then the label they belong to.
666            "CREATE PROPERTY GRAPH g VERTEX TABLES (s PROPERTIES (id, name) LABEL person, \
667             t PROPERTIES (x) LABEL a LABEL b NO PROPERTIES)",
668        ] {
669            let parsed = UrsaStatement::CreatePropertyGraph(Box::new(create(sql)));
670            let printed = parsed.to_string();
671            let mut again = parse_sql(&printed).unwrap();
672            assert_eq!(again.pop().unwrap(), parsed, "{printed}");
673        }
674        assert_eq!(
675            UrsaStatement::CreatePropertyGraph(Box::new(create(GETTING_STARTED))).to_string(),
676            "CREATE PROPERTY GRAPH karate VERTEX TABLES (members KEY (id)) EDGE TABLES (ties \
677             SOURCE KEY (src) REFERENCES members (id) DESTINATION KEY (dst) REFERENCES members (id))"
678        );
679    }
680
681    #[test]
682    fn the_statement_after_a_graph_still_parses() {
683        let sql =
684            "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (a) DESTINATION KEY (b)); SELECT 1";
685        assert_eq!(parse_sql(sql).unwrap().len(), 2);
686    }
687
688    #[test]
689    fn what_is_still_refused() {
690        let multi_column = ("multi-column keys are", ONE_COLUMN_KEYS);
691        let expression = (
692            "a property that is not a column is",
693            "compute it in a view, and list the view's column",
694        );
695        for (sql, (what, hint)) in [
696            (
697                "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))
698                   EDGE TABLES (f SOURCE KEY (s) DESTINATION KEY (d))",
699                (
700                    "a second EDGE TABLES clause is",
701                    "list every edge table in one EDGE TABLES (…)",
702                ),
703            ),
704            (
705                "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))
706                   VERTEX TABLES (v KEY (id))",
707                (
708                    "VERTEX TABLES after EDGE TABLES is",
709                    "write VERTEX TABLES first",
710                ),
711            ),
712            (
713                "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (a, b))
714                   EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))",
715                multi_column,
716            ),
717            (
718                "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s1, s2) DESTINATION KEY (d))",
719                multi_column,
720            ),
721            (
722                "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id))
723                   EDGE TABLES (e SOURCE KEY (s) REFERENCES v (id, x) DESTINATION KEY (d))",
724                multi_column,
725            ),
726            (
727                "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id))
728                   EDGE TABLES (e SOURCE v DESTINATION v)",
729                (
730                    "SOURCE without KEY is",
731                    "name the column with SOURCE KEY (column) REFERENCES vertex_table (key)",
732                ),
733            ),
734            (
735                "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id) PROPERTIES (upper(name)))",
736                expression,
737            ),
738            (
739                "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id) PROPERTIES (a + b))",
740                expression,
741            ),
742            (
743                "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id) LABEL a DEFAULT LABEL)",
744                ("this label clause is", "write LABEL name, once per label"),
745            ),
746        ] {
747            let message = error(sql);
748            // The main clause, then where, then the hint.
749            assert!(
750                message.starts_with(&format!("{what} not supported yet at Line: "))
751                    && message.ends_with(&format!("; {hint}")),
752                "{sql}\n{message}"
753            );
754        }
755    }
756
757    #[test]
758    fn syntax_errors_read_like_datafusions() {
759        for (sql, expected) in [
760            (
761                "CREATE PROPERTY GRAPH g",
762                "syntax error: Expected: VERTEX TABLES or EDGE TABLES, found: EOF",
763            ),
764            (
765                "CREATE PROPERTY GRAPH g EDGE TABLES e SOURCE KEY (s) DESTINATION KEY (d)",
766                "syntax error: Expected: (, found: e at Line: 1, Column: 37",
767            ),
768            (
769                "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s))",
770                "syntax error: Expected: DESTINATION, found: ) at Line: 1, Column: 54",
771            ),
772            (
773                "CREATE PROPERTY GRAPH g EDGE TABLES (e DESTINATION KEY (d) SOURCE KEY (s))",
774                "syntax error: Expected: SOURCE, found: DESTINATION at Line: 1, Column: 40",
775            ),
776            (
777                "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY s DESTINATION KEY (d))",
778                "syntax error: Expected: (, found: s at Line: 1, Column: 51",
779            ),
780            (
781                "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d)",
782                "syntax error: Expected: ), found: EOF",
783            ),
784            (
785                "CREATE PROPERTY GRAPH g VERTEX TABLES (v ID (id)) EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))",
786                "syntax error: Expected: KEY, LABEL, PROPERTIES, ',' or ')', found: ID at Line: 1, Column: 42",
787            ),
788        ] {
789            assert_eq!(error(sql), expected, "{sql}");
790        }
791    }
792}