Skip to main content

reifydb_core/error/diagnostic/
query.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_value::{byte_size::ByteSize, error::Diagnostic, fragment::Fragment};
5
6pub fn memory_limit_exceeded(used: ByteSize, limit: ByteSize) -> Diagnostic {
7	Diagnostic {
8		code: "QUERY_006".to_string(),
9		rql: None,
10		message: format!("query exceeded its memory limit of {} (needs at least {})", limit, used),
11		fragment: Fragment::None,
12		label: Some("this query buffers more data in memory than the per-query limit allows".to_string()),
13		help: Some(
14			"reduce the amount of data the query materializes (narrow the FILTER, project fewer columns, or add a LIMIT), or raise the query memory limit"
15				.to_string(),
16		),
17		column: None,
18		notes: vec![],
19		cause: None,
20		operator_chain: None,
21	}
22}
23
24pub fn column_not_found(fragment: Fragment) -> Diagnostic {
25	Diagnostic {
26		code: "QUERY_001".to_string(),
27		rql: None,
28		message: "column not found".to_string(),
29		fragment,
30		label: Some("this column does not exist in the current context".to_string()),
31		help: Some("check for typos or ensure the column is defined in the input".to_string()),
32		column: None,
33		notes: vec![],
34		cause: None,
35		operator_chain: None,
36	}
37}
38
39pub fn extend_duplicate_column(fragment: Fragment, column_name: &str) -> Diagnostic {
40	Diagnostic {
41		code: "EXTEND_002".to_string(),
42		rql: None,
43		message: format!("Cannot extend with duplicate column name '{}'", column_name),
44		fragment,
45		label: Some("column already exists in the current frame".to_string()),
46		help: Some("Use a different column name or remove the existing column first".to_string()),
47		column: None,
48		notes: vec![
49			"EXTEND operation cannot add columns that already exist in the frame".to_string(),
50			"Each column name must be unique within the result frame".to_string(),
51			"Consider using MAP if you want to replace existing columns".to_string(),
52		],
53		cause: None,
54		operator_chain: None,
55	}
56}
57
58pub fn unsupported_source_qualification(fragment: Fragment, name: &str) -> Diagnostic {
59	Diagnostic {
60		code: "QUERY_002".to_string(),
61		rql: None,
62		message: format!("Source qualification '{}' is not supported in RQL expressions", name),
63		fragment,
64		label: Some("source qualification is only allowed for join aliases in ON clauses".to_string()),
65		help: Some("Remove the qualification or use it only with a join alias in the ON clause".to_string()),
66		column: None,
67		notes: vec![
68			"RQL uses a dataframe-centered approach where each operation produces a new dataframe"
69				.to_string(),
70			"Source qualifications are not needed as columns are unambiguous in the dataframe".to_string(),
71			"Only join aliases can be used for qualification within ON clauses to disambiguate columns"
72				.to_string(),
73		],
74		cause: None,
75		operator_chain: None,
76	}
77}
78
79pub fn system_column_read_only(fragment: Fragment) -> Diagnostic {
80	let name = fragment.text();
81	Diagnostic {
82		code: "QUERY_004".to_string(),
83		rql: None,
84		message: format!("system column '{}' is read-only", name),
85		fragment,
86		label: Some("system columns are managed automatically and cannot be set".to_string()),
87		help: Some("remove this field from the INSERT/UPDATE values".to_string()),
88		column: None,
89		notes: vec![],
90		cause: None,
91		operator_chain: None,
92	}
93}
94
95pub fn system_column_reserved(fragment: Fragment) -> Diagnostic {
96	let name = fragment.text();
97	Diagnostic {
98		code: "QUERY_016".to_string(),
99		rql: None,
100		message: format!("column name '{}' is reserved for system columns", name),
101		fragment,
102		label: Some("names starting with '#' always refer to system columns".to_string()),
103		help: Some("rename the column so it does not start with '#'".to_string()),
104		column: None,
105		notes: vec![],
106		cause: None,
107		operator_chain: None,
108	}
109}
110
111pub fn empty_map(fragment: Fragment) -> Diagnostic {
112	Diagnostic {
113		code: "QUERY_014".to_string(),
114		rql: None,
115		message: "MAP needs at least one field".to_string(),
116		fragment,
117		label: Some("MAP with no fields would return rows with no columns".to_string()),
118		help: Some("list the fields to keep, e.g. MAP { x }".to_string()),
119		column: None,
120		notes: vec![],
121		cause: None,
122		operator_chain: None,
123	}
124}
125
126pub fn duplicate_field(fragment: Fragment, name: &str) -> Diagnostic {
127	Diagnostic {
128		code: "QUERY_009".to_string(),
129		rql: None,
130		message: format!("field '{}' is given more than once", name),
131		fragment,
132		label: Some("a row, constructor, MAP or UPDATE may give each field only once".to_string()),
133		help: Some("remove one of the values for this field".to_string()),
134		column: None,
135		notes: vec![],
136		cause: None,
137		operator_chain: None,
138	}
139}
140
141pub fn as_clause_not_query(fragment: Fragment, kind: &str) -> Diagnostic {
142	Diagnostic {
143		code: "QUERY_005".to_string(),
144		rql: None,
145		message: format!("AS clause body must be a query, found {}", kind),
146		fragment,
147		label: Some("expected a query expression here".to_string()),
148		help: Some(
149			"AS { ... } accepts only query expressions (FROM, MAP, FILTER, JOIN, ...); nested DDL statements such as CREATE/DROP/ALTER are not allowed"
150				.to_string(),
151		),
152		column: None,
153		notes: vec![],
154		cause: None,
155		operator_chain: None,
156	}
157}
158
159pub fn not_a_query_input(fragment: Fragment, kind: &str) -> Diagnostic {
160	Diagnostic {
161		code: "QUERY_010".to_string(),
162		rql: None,
163		message: format!("{} cannot be the input of a query step", kind),
164		fragment,
165		label: Some("a query was expected here".to_string()),
166		help: Some(format!(
167			"a step such as MAP or FILTER reads from a query; run the {} as its own statement",
168			kind
169		)),
170		column: None,
171		notes: vec![],
172		cause: None,
173		operator_chain: None,
174	}
175}
176
177pub fn window_requires_deferred_view(fragment: Fragment) -> Diagnostic {
178	Diagnostic {
179		code: "QUERY_007".to_string(),
180		rql: None,
181		message: "window runs only in deferred views, not in a batch query".to_string(),
182		fragment,
183		label: Some("a batch query cannot run a window".to_string()),
184		help: Some(
185			"define the window in CREATE DEFERRED VIEW ns::name AS { ... } and read the view with FROM ns::name"
186				.to_string(),
187		),
188		column: None,
189		notes: vec![],
190		cause: None,
191		operator_chain: None,
192	}
193}
194
195pub fn append_requires_deferred_view(fragment: Fragment) -> Diagnostic {
196	Diagnostic {
197		code: "QUERY_011".to_string(),
198		rql: None,
199		message: "append runs only in deferred views, not in a batch query".to_string(),
200		fragment,
201		label: Some("a batch query cannot run an append".to_string()),
202		help: Some(
203			"define the append in CREATE DEFERRED VIEW ns::name AS { ... } and read the view with FROM ns::name"
204				.to_string(),
205		),
206		column: None,
207		notes: vec![],
208		cause: None,
209		operator_chain: None,
210	}
211}
212
213pub fn no_column_block(fragment: Fragment, kind: &str, name: &str) -> Diagnostic {
214	let help = match kind {
215		"series" => format!("wait for the bucket to seal, or read the {} with a regular query", kind),
216		_ => format!("wait for the column store to materialize the {}, or read it with a regular query", kind),
217	};
218	Diagnostic {
219		code: "QUERY_012".to_string(),
220		rql: None,
221		message: format!("no column block for {} '{}'", kind, name),
222		fragment,
223		label: Some(format!("this {} has not been materialized into the column store yet", kind)),
224		help: Some(help),
225		column: None,
226		notes: vec![],
227		cause: None,
228		operator_chain: None,
229	}
230}
231
232pub fn unsupported_in_column_layout(what: &str) -> Diagnostic {
233	Diagnostic {
234		code: "QUERY_013".to_string(),
235		rql: None,
236		message: format!("{} cannot be read from the column store", what),
237		fragment: Fragment::None,
238		label: Some("a column query reads only materialized table blocks".to_string()),
239		help: Some("run this query with a regular query instead".to_string()),
240		column: None,
241		notes: vec![],
242		cause: None,
243		operator_chain: None,
244	}
245}
246
247pub fn unknown_apply_operator(fragment: Fragment) -> Diagnostic {
248	let name = fragment.text().to_string();
249	Diagnostic {
250		code: "QUERY_008".to_string(),
251		rql: None,
252		message: format!("unknown operator '{}'", name),
253		fragment,
254		label: Some("no operator with this name is registered".to_string()),
255		help: Some("check the operator name for typos, or register the operator before applying it".to_string()),
256		column: None,
257		notes: vec![],
258		cause: None,
259		operator_chain: None,
260	}
261}
262
263#[cfg(test)]
264mod tests {
265	use super::*;
266
267	#[test]
268	fn memory_limit_exceeded_has_stable_code_and_fields() {
269		let d = memory_limit_exceeded(ByteSize::from_kib(5), ByteSize::from_kib(4));
270		assert_eq!(d.code, "QUERY_006");
271		assert!(matches!(d.fragment, Fragment::None));
272		assert!(d.message.contains("exceeded its memory limit of 4 KiB"), "unexpected message: {}", d.message);
273		assert!(d.message.contains("needs at least 5 KiB"), "unexpected message: {}", d.message);
274		assert!(d.label.unwrap().contains("buffers more data in memory"));
275		assert!(d.help.unwrap().contains("raise the query memory limit"));
276	}
277}