1use std::fmt;
21
22use datafusion::error::{DataFusionError, Result};
23use datafusion::sql::sqlparser::ast::{Ident, ObjectName};
24use datafusion::sql::sqlparser::parser::Parser;
25use datafusion::sql::sqlparser::tokenizer::Token;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct VertexTableDef {
30 pub table: ObjectName,
31 pub key: Ident,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct EdgeTableDef {
37 pub table: ObjectName,
38 pub source: EndpointDef,
39 pub destination: EndpointDef,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct EndpointDef {
45 pub key: Ident,
46 pub references: Option<References>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct References {
52 pub table: ObjectName,
53 pub column: Ident,
54}
55
56impl fmt::Display for VertexTableDef {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(f, "VERTEX TABLES ({} KEY ({}))", self.table, self.key)
59 }
60}
61
62impl fmt::Display for EdgeTableDef {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 write!(
65 f,
66 "EDGE TABLES ({} SOURCE {} DESTINATION {})",
67 self.table, self.source, self.destination
68 )
69 }
70}
71
72impl fmt::Display for EndpointDef {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(f, "KEY ({})", self.key)?;
75 if let Some(r) = &self.references {
76 write!(f, " REFERENCES {} ({})", r.table, r.column)?;
77 }
78 Ok(())
79 }
80}
81
82const ONE_TABLE_EACH: &str = "Ursa graphs have one edge table and at most one vertex table";
84
85pub(crate) fn parse_tables(p: &mut Parser) -> Result<(Option<VertexTableDef>, EdgeTableDef)> {
87 let vertices =
88 if consume_words(p, &["VERTEX", "TABLES"]) || consume_words(p, &["NODE", "TABLES"]) {
89 p.expect_token(&Token::LParen)?;
90 let vertices = parse_vertex_table(p)?;
91 if p.peek_token_ref().token == Token::Comma {
92 return unsupported(p, "several vertex tables are");
93 }
94 p.expect_token(&Token::RParen)?;
95 Some(vertices)
96 } else {
97 None
98 };
99 if !(consume_words(p, &["EDGE", "TABLES"]) || consume_words(p, &["RELATIONSHIP", "TABLES"])) {
100 let expected = if vertices.is_some() {
101 "EDGE TABLES"
102 } else {
103 "VERTEX TABLES or EDGE TABLES"
104 };
105 return Ok(p.expected_ref(expected, p.peek_token_ref())?);
106 }
107 p.expect_token(&Token::LParen)?;
108 let edges = parse_edge_table(p)?;
109 if p.peek_token_ref().token == Token::Comma {
110 return unsupported(p, "several edge tables are");
111 }
112 p.expect_token(&Token::RParen)?;
113 if word(p, 0, "VERTEX") || word(p, 0, "NODE") {
114 return unsupported(p, "VERTEX TABLES after EDGE TABLES is");
115 }
116 if word(p, 0, "EDGE") || word(p, 0, "RELATIONSHIP") {
117 return unsupported(p, "several EDGE TABLES clauses are");
118 }
119 Ok((vertices, edges))
120}
121
122fn parse_vertex_table(p: &mut Parser) -> Result<VertexTableDef> {
123 let table = p.parse_object_name(false)?;
124 reject_element_options(p)?;
125 if !consume_words(p, &["KEY"]) {
126 if matches!(p.peek_token_ref().token, Token::RParen | Token::Comma) {
127 return unsupported(
128 p,
129 "a vertex table without KEY is (name its key column with KEY (column))",
130 );
131 }
132 return Ok(p.expected_ref("KEY", p.peek_token_ref())?);
133 }
134 let key = parse_key(p)?;
135 reject_element_options(p)?;
136 Ok(VertexTableDef { table, key })
137}
138
139fn parse_edge_table(p: &mut Parser) -> Result<EdgeTableDef> {
140 let table = p.parse_object_name(false)?;
141 reject_element_options(p)?;
142 if word(p, 0, "KEY") {
143 return unsupported(p, "an edge table KEY is");
144 }
145 expect_word(p, "SOURCE")?;
146 let source = parse_endpoint(p, "SOURCE")?;
147 expect_word(p, "DESTINATION")?;
148 let destination = parse_endpoint(p, "DESTINATION")?;
149 reject_element_options(p)?;
150 Ok(EdgeTableDef {
151 table,
152 source,
153 destination,
154 })
155}
156
157fn parse_endpoint(p: &mut Parser, side: &str) -> Result<EndpointDef> {
158 if !consume_words(p, &["KEY"]) {
159 if matches!(p.peek_token_ref().token, Token::Word(_)) {
160 return unsupported(
161 p,
162 &format!(
163 "{side} without KEY is (name the column with {side} KEY (column) \
164 REFERENCES vertex_table (key))"
165 ),
166 );
167 }
168 return Ok(p.expected_ref("KEY", p.peek_token_ref())?);
169 }
170 let key = parse_key(p)?;
171 let references = if consume_words(p, &["REFERENCES"]) {
172 let table = p.parse_object_name(false)?;
173 let column = parse_key(p)?;
174 Some(References { table, column })
175 } else {
176 None
177 };
178 Ok(EndpointDef { key, references })
179}
180
181fn parse_key(p: &mut Parser) -> Result<Ident> {
183 p.expect_token(&Token::LParen)?;
184 let column = p.parse_identifier()?;
185 if p.peek_token_ref().token == Token::Comma {
186 return unsupported(
187 p,
188 "multi-column keys are (KEY, SOURCE KEY, DESTINATION KEY and REFERENCES take one column)",
189 );
190 }
191 p.expect_token(&Token::RParen)?;
192 Ok(column)
193}
194
195fn reject_element_options(p: &mut Parser) -> Result<()> {
198 if word(p, 0, "AS") {
199 return unsupported(p, "table aliases (AS) are");
200 }
201 if word(p, 0, "LABEL") || (word(p, 0, "DEFAULT") && word(p, 1, "LABEL")) {
202 return unsupported(p, "labels (LABEL, DEFAULT LABEL) are");
203 }
204 if word(p, 0, "PROPERTIES") || (word(p, 0, "NO") && word(p, 1, "PROPERTIES")) {
205 return unsupported(
206 p,
207 "PROPERTIES clauses are (every column of the vertex and edge tables is a property)",
208 );
209 }
210 Ok(())
211}
212
213fn unsupported<T>(p: &Parser, what: &str) -> Result<T> {
215 Err(DataFusionError::NotImplemented(format!(
216 "{what} not supported yet: {ONE_TABLE_EACH}{}",
217 p.peek_token_ref().span.start
218 )))
219}
220
221pub(crate) fn word(p: &Parser, n: usize, word: &str) -> bool {
223 match &p.peek_nth_token_ref(n).token {
224 Token::Word(w) => w.quote_style.is_none() && w.value.eq_ignore_ascii_case(word),
225 _ => false,
226 }
227}
228
229pub(crate) fn consume_words(p: &mut Parser, words: &[&str]) -> bool {
231 let found = words.iter().enumerate().all(|(i, w)| word(p, i, w));
232 if found {
233 for _ in words {
234 p.next_token();
235 }
236 }
237 found
238}
239
240fn expect_word(p: &mut Parser, expected: &str) -> Result<()> {
241 if consume_words(p, &[expected]) {
242 Ok(())
243 } else {
244 Ok(p.expected_ref(expected, p.peek_token_ref())?)
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use datafusion::sql::sqlparser::ast::ObjectNamePart;
251
252 use super::*;
253 use crate::{parse_sql, CreatePropertyGraph, UrsaStatement};
254
255 fn create(sql: &str) -> CreatePropertyGraph {
256 let mut statements = parse_sql(sql).unwrap_or_else(|e| panic!("{sql}: {e}"));
257 assert_eq!(statements.len(), 1, "{sql}");
258 match statements.pop().unwrap() {
259 UrsaStatement::CreatePropertyGraph(g) => *g,
260 other => panic!("{sql} parsed as {other:?}"),
261 }
262 }
263
264 fn error(sql: &str) -> String {
265 match parse_sql(sql) {
266 Ok(statements) => panic!("{sql} should not parse; got {statements:?}"),
267 Err(e) => ursa_engine::user_message(&e),
268 }
269 }
270
271 fn name(parts: &[&str]) -> ObjectName {
272 ObjectName(
273 parts
274 .iter()
275 .map(|p| ObjectNamePart::Identifier(Ident::new(*p)))
276 .collect(),
277 )
278 }
279
280 fn endpoint(key: &str, references: Option<(&str, &str)>) -> EndpointDef {
281 EndpointDef {
282 key: Ident::new(key),
283 references: references.map(|(table, column)| References {
284 table: name(&[table]),
285 column: Ident::new(column),
286 }),
287 }
288 }
289
290 const GETTING_STARTED: &str = "CREATE PROPERTY GRAPH karate
291 VERTEX TABLES (members KEY (id))
292 EDGE TABLES (
293 ties SOURCE KEY (src) REFERENCES members (id)
294 DESTINATION KEY (dst) REFERENCES members (id)
295 )";
296
297 #[test]
298 fn the_getting_started_graph() {
299 assert_eq!(
300 create(GETTING_STARTED),
301 CreatePropertyGraph {
302 name: name(&["karate"]),
303 or_replace: false,
304 if_not_exists: false,
305 vertices: Some(VertexTableDef {
306 table: name(&["members"]),
307 key: Ident::new("id"),
308 }),
309 edges: EdgeTableDef {
310 table: name(&["ties"]),
311 source: endpoint("src", Some(("members", "id"))),
312 destination: endpoint("dst", Some(("members", "id"))),
313 },
314 }
315 );
316 }
317
318 #[test]
319 fn accepted_forms() {
320 let g = create(
321 "CREATE PROPERTY GRAPH karate_edges_only
322 EDGE TABLES (ties SOURCE KEY (src) DESTINATION KEY (dst))",
323 );
324 assert_eq!(g.vertices, None);
325 assert_eq!(g.edges.source, endpoint("src", None));
326 assert_eq!(g.edges.destination, endpoint("dst", None));
327
328 let g = create(
330 r#"create or replace property graph if not exists "Social"
331 node tables (db.public."People" key ("Id"))
332 relationship tables (db.follows source key (a) references db.public."People" ("Id")
333 destination key (b))"#,
334 );
335 assert!(g.or_replace && g.if_not_exists);
336 assert_eq!(g.name.to_string(), r#""Social""#);
337 let vertices = g.vertices.unwrap();
338 assert_eq!(vertices.table.to_string(), r#"db.public."People""#);
339 assert_eq!(vertices.key, Ident::with_quote('"', "Id"));
340 assert_eq!(g.edges.table.to_string(), "db.follows");
341 assert_eq!(
342 g.edges.source.references.unwrap().table.to_string(),
343 r#"db.public."People""#
344 );
345 assert_eq!(g.edges.destination.references, None);
346 }
347
348 #[test]
349 fn display_round_trips() {
350 for sql in [
351 GETTING_STARTED,
352 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (a) DESTINATION KEY (b))",
353 r#"CREATE OR REPLACE PROPERTY GRAPH IF NOT EXISTS "G" VERTEX TABLES (s.v KEY ("K"))
354 EDGE TABLES (s.e SOURCE KEY (a) DESTINATION KEY (b) REFERENCES s.v ("K"))"#,
355 ] {
356 let parsed = UrsaStatement::CreatePropertyGraph(Box::new(create(sql)));
357 let printed = parsed.to_string();
358 let mut again = parse_sql(&printed).unwrap();
359 assert_eq!(again.pop().unwrap(), parsed, "{printed}");
360 }
361 assert_eq!(
362 UrsaStatement::CreatePropertyGraph(Box::new(create(GETTING_STARTED))).to_string(),
363 "CREATE PROPERTY GRAPH karate VERTEX TABLES (members KEY (id)) EDGE TABLES (ties \
364 SOURCE KEY (src) REFERENCES members (id) DESTINATION KEY (dst) REFERENCES members (id))"
365 );
366 }
367
368 #[test]
369 fn the_statement_after_a_graph_still_parses() {
370 let sql =
371 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (a) DESTINATION KEY (b)); SELECT 1";
372 assert_eq!(parse_sql(sql).unwrap().len(), 2);
373 }
374
375 #[test]
376 fn sql_pgq_beyond_the_subset_is_rejected() {
377 let not_supported = "not supported yet: Ursa graphs have one edge table and at most one \
378 vertex table";
379 let properties =
380 "PROPERTIES clauses are (every column of the vertex and edge tables is a property)";
381 let multi_column =
382 "multi-column keys are (KEY, SOURCE KEY, DESTINATION KEY and REFERENCES take one column)";
383 for (sql, what) in [
384 (
385 "CREATE PROPERTY GRAPH g VERTEX TABLES (a KEY (id), b KEY (id))
386 EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))",
387 "several vertex tables are",
388 ),
389 (
390 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d),
391 f SOURCE KEY (s) DESTINATION KEY (d))",
392 "several edge tables are",
393 ),
394 (
395 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))
396 EDGE TABLES (f SOURCE KEY (s) DESTINATION KEY (d))",
397 "several EDGE TABLES clauses are",
398 ),
399 (
400 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))
401 VERTEX TABLES (v KEY (id))",
402 "VERTEX TABLES after EDGE TABLES is",
403 ),
404 (
405 "CREATE PROPERTY GRAPH g VERTEX TABLES (members AS m KEY (id))
406 EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))",
407 "table aliases (AS) are",
408 ),
409 (
410 "CREATE PROPERTY GRAPH g EDGE TABLES (e AS ties SOURCE KEY (s) DESTINATION KEY (d))",
411 "table aliases (AS) are",
412 ),
413 (
414 "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id) LABEL person)
415 EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))",
416 "labels (LABEL, DEFAULT LABEL) are",
417 ),
418 (
419 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d) DEFAULT LABEL)",
420 "labels (LABEL, DEFAULT LABEL) are",
421 ),
422 (
423 "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id) PROPERTIES (name))
424 EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))",
425 properties,
426 ),
427 (
428 "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id) PROPERTIES ARE ALL COLUMNS)
429 EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))",
430 properties,
431 ),
432 (
433 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d) NO PROPERTIES)",
434 properties,
435 ),
436 (
437 "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (a, b))
438 EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))",
439 multi_column,
440 ),
441 (
442 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s1, s2) DESTINATION KEY (d))",
443 multi_column,
444 ),
445 (
446 "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id))
447 EDGE TABLES (e SOURCE KEY (s) REFERENCES v (id, x) DESTINATION KEY (d))",
448 multi_column,
449 ),
450 (
451 "CREATE PROPERTY GRAPH g EDGE TABLES (e KEY (id) SOURCE KEY (s) DESTINATION KEY (d))",
452 "an edge table KEY is",
453 ),
454 (
455 "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id))
456 EDGE TABLES (e SOURCE v DESTINATION v)",
457 "SOURCE without KEY is (name the column with SOURCE KEY (column) REFERENCES \
458 vertex_table (key))",
459 ),
460 (
461 "CREATE PROPERTY GRAPH g VERTEX TABLES (v)
462 EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))",
463 "a vertex table without KEY is (name its key column with KEY (column))",
464 ),
465 ] {
466 let message = error(sql);
467 assert!(
468 message.starts_with(&format!("{what} {not_supported} at Line: ")),
469 "{sql}\n{message}"
470 );
471 }
472 }
473
474 #[test]
475 fn syntax_errors_read_like_datafusions() {
476 for (sql, expected) in [
477 (
478 "CREATE PROPERTY GRAPH g",
479 "syntax error: Expected: VERTEX TABLES or EDGE TABLES, found: EOF",
480 ),
481 (
482 "CREATE PROPERTY GRAPH g VERTEX TABLES (v KEY (id))",
483 "syntax error: Expected: EDGE TABLES, found: EOF",
484 ),
485 (
486 "CREATE PROPERTY GRAPH g EDGE TABLES e SOURCE KEY (s) DESTINATION KEY (d)",
487 "syntax error: Expected: (, found: e at Line: 1, Column: 37",
488 ),
489 (
490 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s))",
491 "syntax error: Expected: DESTINATION, found: ) at Line: 1, Column: 54",
492 ),
493 (
494 "CREATE PROPERTY GRAPH g EDGE TABLES (e DESTINATION KEY (d) SOURCE KEY (s))",
495 "syntax error: Expected: SOURCE, found: DESTINATION at Line: 1, Column: 40",
496 ),
497 (
498 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY s DESTINATION KEY (d))",
499 "syntax error: Expected: (, found: s at Line: 1, Column: 51",
500 ),
501 (
502 "CREATE PROPERTY GRAPH g EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d)",
503 "syntax error: Expected: ), found: EOF",
504 ),
505 (
506 "CREATE PROPERTY GRAPH g VERTEX TABLES (v ID (id)) EDGE TABLES (e SOURCE KEY (s) DESTINATION KEY (d))",
507 "syntax error: Expected: KEY, found: ID at Line: 1, Column: 42",
508 ),
509 ] {
510 assert_eq!(error(sql), expected, "{sql}");
511 }
512 }
513}