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 parse_auto_with_language(input).map(|(_, query)| query)
38}
39
40pub fn parse_auto_with_language(input: &str) -> Result<(QueryLanguage, GqlQuery), QueryError> {
46 let trimmed = input.trim();
47 reject_write(trimmed)?;
48 let language = if trimmed
49 .as_bytes()
50 .get(..6)
51 .is_some_and(|p| p.eq_ignore_ascii_case(b"SELECT"))
52 {
53 QueryLanguage::Sparql
54 } else {
55 QueryLanguage::Gql
56 };
57 parse(language, trimmed).map(|query| (language, query))
58}
59
60fn reject_write(input: &str) -> Result<(), QueryError> {
62 match leading_keyword(input).as_str() {
63 "CREATE" | "DELETE" | "DETACH" | "SET" | "REMOVE" | "MERGE" | "INSERT" | "UPDATE"
64 | "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]
81 fn parse_auto_with_delete_rejected() {
82 let err = parse_auto("WITH <http://g> DELETE { ?s ?p ?o } WHERE { ?s ?p ?o }").unwrap_err();
83 assert!(
84 matches!(err, QueryError::Unsupported(_)),
85 "WITH … DELETE must return Unsupported on the public path; got {err:?}"
86 );
87 let msg = err.to_string();
88 assert!(msg.contains("read-only"), "got: {msg}");
89 assert!(
90 msg.contains("create") && msg.contains("update") && msg.contains("delete"),
91 "error must name the mutation verbs; got: {msg}"
92 );
93 }
94
95 #[test]
96 fn parse_auto_prefixed_insert_data_rejected() {
97 let err = parse_auto("PREFIX ex: <http://e/> INSERT DATA { ex:a ex:b ex:c }").unwrap_err();
98 assert!(
99 matches!(err, QueryError::Unsupported(_)),
100 "prefixed INSERT DATA must return Unsupported on the public path; got {err:?}"
101 );
102 let msg = err.to_string();
103 assert!(msg.contains("read-only"), "got: {msg}");
104 }
105
106 #[test]
107 fn parse_auto_prefixed_with_delete_rejected() {
108 let err = parse_auto(
109 "PREFIX ex: <http://e/> WITH <http://g> DELETE { ?s ?p ?o } WHERE { ?s ?p ?o }",
110 )
111 .unwrap_err();
112 assert!(
113 matches!(err, QueryError::Unsupported(_)),
114 "PREFIX + WITH … DELETE must return Unsupported on the public path; got {err:?}"
115 );
116 let msg = err.to_string();
117 assert!(msg.contains("read-only"), "got: {msg}");
118 }
119
120 #[test]
121 fn parse_auto_detach_delete_rejected() {
122 let err = parse_auto("DETACH DELETE (n)").unwrap_err();
123 assert!(
124 matches!(err, QueryError::Unsupported(_)),
125 "DETACH DELETE must return Unsupported on the public path; got {err:?}"
126 );
127 let msg = err.to_string();
128 assert!(msg.contains("read-only"), "got: {msg}");
129 }
130
131 #[test]
132 fn parse_auto_gql_match_not_rejected() {
133 let q = parse_auto("MATCH (a:concept) RETURN a").unwrap();
134 assert!(!q.pattern.elements.is_empty(), "valid GQL MATCH must parse");
135 }
136
137 #[test]
138 fn parse_auto_sparql_select_not_rejected() {
139 let q = parse_auto("SELECT ?a WHERE { ?a :extends ?b . }").unwrap();
140 assert!(
141 !q.pattern.elements.is_empty(),
142 "valid SPARQL SELECT must parse"
143 );
144 }
145
146 #[test]
147 fn parse_auto_with_language_reports_selected_parser() {
148 let (gql_language, _) = parse_auto_with_language("MATCH (a:concept) RETURN a").unwrap();
149 assert_eq!(gql_language, QueryLanguage::Gql);
150
151 let (sparql_language, _) =
152 parse_auto_with_language("SELECT ?a WHERE { ?a :extends ?b . }").unwrap();
153 assert_eq!(sparql_language, QueryLanguage::Sparql);
154 }
155
156 #[test]
157 fn parse_auto_load_rejected() {
158 let err = parse_auto("LOAD <http://e/data>").unwrap_err();
159 assert!(
160 matches!(err, QueryError::Unsupported(_)),
161 "LOAD must return Unsupported on the public path; got {err:?}"
162 );
163 let msg = err.to_string();
164 assert!(msg.contains("read-only"), "got: {msg}");
165 }
166}