1use crate::ast::GqlQuery;
4use crate::error::QueryError;
5use crate::parsers;
6use crate::parsers::sparql::leading_keyword;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum QueryLanguage {
11 Gql,
12 Sparql,
13}
14
15pub fn parse(language: QueryLanguage, input: &str) -> Result<GqlQuery, QueryError> {
22 match language {
23 QueryLanguage::Gql => parsers::gql::parse(input),
24 QueryLanguage::Sparql => parsers::sparql::parse(input),
25 }
26}
27
28pub fn parse_auto(input: &str) -> Result<GqlQuery, QueryError> {
37 let trimmed = input.trim();
38 reject_write(trimmed)?;
39 if trimmed
40 .as_bytes()
41 .get(..6)
42 .is_some_and(|p| p.eq_ignore_ascii_case(b"SELECT"))
43 {
44 parsers::sparql::parse(trimmed)
45 } else if trimmed
46 .as_bytes()
47 .get(..5)
48 .is_some_and(|p| p.eq_ignore_ascii_case(b"MATCH"))
49 {
50 parsers::gql::parse(trimmed)
51 } else {
52 parsers::gql::parse(trimmed)
54 }
55}
56
57fn reject_write(input: &str) -> Result<(), QueryError> {
59 match leading_keyword(input).as_str() {
60 "CREATE" | "DELETE" | "DETACH" | "SET" | "REMOVE" | "MERGE" | "INSERT" | "UPDATE"
61 | "WITH" | "LOAD" | "CLEAR" | "DROP" | "COPY" | "MOVE" | "ADD" => {
62 Err(QueryError::Unsupported(
63 "the query verb is read-only; \
64 to mutate the graph use: create, update, link, merge, delete"
65 .into(),
66 ))
67 }
68 _ => Ok(()),
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::error::QueryError;
76
77 #[test]
78 fn parse_auto_with_delete_rejected() {
79 let err = parse_auto("WITH <http://g> DELETE { ?s ?p ?o } WHERE { ?s ?p ?o }").unwrap_err();
80 assert!(
81 matches!(err, QueryError::Unsupported(_)),
82 "WITH … DELETE must return Unsupported on the public path; got {err:?}"
83 );
84 let msg = err.to_string();
85 assert!(msg.contains("read-only"), "got: {msg}");
86 assert!(
87 msg.contains("create") && msg.contains("update") && msg.contains("delete"),
88 "error must name the mutation verbs; got: {msg}"
89 );
90 }
91
92 #[test]
93 fn parse_auto_prefixed_insert_data_rejected() {
94 let err = parse_auto("PREFIX ex: <http://e/> INSERT DATA { ex:a ex:b ex:c }").unwrap_err();
95 assert!(
96 matches!(err, QueryError::Unsupported(_)),
97 "prefixed INSERT DATA must return Unsupported on the public path; got {err:?}"
98 );
99 let msg = err.to_string();
100 assert!(msg.contains("read-only"), "got: {msg}");
101 }
102
103 #[test]
104 fn parse_auto_prefixed_with_delete_rejected() {
105 let err = parse_auto(
106 "PREFIX ex: <http://e/> WITH <http://g> DELETE { ?s ?p ?o } WHERE { ?s ?p ?o }",
107 )
108 .unwrap_err();
109 assert!(
110 matches!(err, QueryError::Unsupported(_)),
111 "PREFIX + WITH … DELETE must return Unsupported on the public path; got {err:?}"
112 );
113 let msg = err.to_string();
114 assert!(msg.contains("read-only"), "got: {msg}");
115 }
116
117 #[test]
118 fn parse_auto_detach_delete_rejected() {
119 let err = parse_auto("DETACH DELETE (n)").unwrap_err();
120 assert!(
121 matches!(err, QueryError::Unsupported(_)),
122 "DETACH DELETE must return Unsupported on the public path; got {err:?}"
123 );
124 let msg = err.to_string();
125 assert!(msg.contains("read-only"), "got: {msg}");
126 }
127
128 #[test]
129 fn parse_auto_gql_match_not_rejected() {
130 let q = parse_auto("MATCH (a:concept) RETURN a").unwrap();
131 assert!(!q.pattern.elements.is_empty(), "valid GQL MATCH must parse");
132 }
133
134 #[test]
135 fn parse_auto_sparql_select_not_rejected() {
136 let q = parse_auto("SELECT ?a WHERE { ?a :extends ?b . }").unwrap();
137 assert!(
138 !q.pattern.elements.is_empty(),
139 "valid SPARQL SELECT must parse"
140 );
141 }
142
143 #[test]
144 fn parse_auto_load_rejected() {
145 let err = parse_auto("LOAD <http://e/data>").unwrap_err();
146 assert!(
147 matches!(err, QueryError::Unsupported(_)),
148 "LOAD must return Unsupported on the public path; got {err:?}"
149 );
150 let msg = err.to_string();
151 assert!(msg.contains("read-only"), "got: {msg}");
152 }
153}