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