1use 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#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct VertexTableDef {
42 pub table: ObjectName,
43 pub alias: Option<Ident>,
45 pub key: Option<Ident>,
47 pub labels: Vec<LabelDef>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct EdgeTableDef {
53 pub table: ObjectName,
54 pub alias: Option<Ident>,
55 pub key: Option<Ident>,
57 pub source: EndpointDef,
58 pub destination: EndpointDef,
59 pub labels: Vec<LabelDef>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct EndpointDef {
65 pub key: Ident,
66 pub references: Option<References>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct References {
72 pub table: ObjectName,
73 pub column: Ident,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct LabelDef {
81 pub name: Option<Ident>,
82 pub properties: PropertiesDef,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum PropertiesDef {
88 Default,
90 AllColumns { except: Vec<Ident> },
92 List(Vec<(Ident, Option<Ident>)>),
94 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
188pub(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
203const NOT_SUPPORTED: &str = "not supported yet";
205
206pub(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
239fn 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
250fn 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
324fn 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 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
365fn 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
412fn 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
423const ONE_COLUMN_KEYS: &str = "KEY, SOURCE KEY, DESTINATION KEY and REFERENCES take one column";
425
426fn 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
436pub(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
444pub(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 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 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 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 "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 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}