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