reifydb-core 0.10.1

Core database interfaces and data structures for ReifyDB
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 ReifyDB

use reifydb_value::{byte_size::ByteSize, error::Diagnostic, fragment::Fragment};

pub fn memory_limit_exceeded(used: ByteSize, limit: ByteSize) -> Diagnostic {
	Diagnostic {
		code: "QUERY_006".to_string(),
		rql: None,
		message: format!("query exceeded its memory limit of {} (needs at least {})", limit, used),
		fragment: Fragment::None,
		label: Some("this query buffers more data in memory than the per-query limit allows".to_string()),
		help: Some(
			"reduce the amount of data the query materializes (narrow the FILTER, project fewer columns, or add a LIMIT), or raise the query memory limit"
				.to_string(),
		),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn column_not_found(fragment: Fragment) -> Diagnostic {
	Diagnostic {
		code: "QUERY_001".to_string(),
		rql: None,
		message: "column not found".to_string(),
		fragment,
		label: Some("this column does not exist in the current context".to_string()),
		help: Some("check for typos or ensure the column is defined in the input".to_string()),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn extend_duplicate_column(fragment: Fragment, column_name: &str) -> Diagnostic {
	Diagnostic {
		code: "EXTEND_002".to_string(),
		rql: None,
		message: format!("Cannot extend with duplicate column name '{}'", column_name),
		fragment,
		label: Some("column already exists in the current frame".to_string()),
		help: Some("Use a different column name or remove the existing column first".to_string()),
		column: None,
		notes: vec![
			"EXTEND operation cannot add columns that already exist in the frame".to_string(),
			"Each column name must be unique within the result frame".to_string(),
			"Consider using MAP if you want to replace existing columns".to_string(),
		],
		cause: None,
		operator_chain: None,
	}
}

pub fn unsupported_source_qualification(fragment: Fragment, name: &str) -> Diagnostic {
	Diagnostic {
		code: "QUERY_002".to_string(),
		rql: None,
		message: format!("Source qualification '{}' is not supported in RQL expressions", name),
		fragment,
		label: Some("source qualification is only allowed for join aliases in ON clauses".to_string()),
		help: Some("Remove the qualification or use it only with a join alias in the ON clause".to_string()),
		column: None,
		notes: vec![
			"RQL uses a dataframe-centered approach where each operation produces a new dataframe"
				.to_string(),
			"Source qualifications are not needed as columns are unambiguous in the dataframe".to_string(),
			"Only join aliases can be used for qualification within ON clauses to disambiguate columns"
				.to_string(),
		],
		cause: None,
		operator_chain: None,
	}
}

pub fn system_column_read_only(fragment: Fragment) -> Diagnostic {
	let name = fragment.text();
	Diagnostic {
		code: "QUERY_004".to_string(),
		rql: None,
		message: format!("system column '{}' is read-only", name),
		fragment,
		label: Some("system columns are managed automatically and cannot be set".to_string()),
		help: Some("remove this field from the INSERT/UPDATE values".to_string()),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn system_column_reserved(fragment: Fragment) -> Diagnostic {
	let name = fragment.text();
	Diagnostic {
		code: "QUERY_016".to_string(),
		rql: None,
		message: format!("column name '{}' is reserved for system columns", name),
		fragment,
		label: Some("names starting with '#' always refer to system columns".to_string()),
		help: Some("rename the column so it does not start with '#'".to_string()),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn empty_map(fragment: Fragment) -> Diagnostic {
	Diagnostic {
		code: "QUERY_014".to_string(),
		rql: None,
		message: "MAP needs at least one field".to_string(),
		fragment,
		label: Some("MAP with no fields would return rows with no columns".to_string()),
		help: Some("list the fields to keep, e.g. MAP { x }".to_string()),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn duplicate_field(fragment: Fragment, name: &str) -> Diagnostic {
	Diagnostic {
		code: "QUERY_009".to_string(),
		rql: None,
		message: format!("field '{}' is given more than once", name),
		fragment,
		label: Some("a row, constructor, MAP or UPDATE may give each field only once".to_string()),
		help: Some("remove one of the values for this field".to_string()),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn as_clause_not_query(fragment: Fragment, kind: &str) -> Diagnostic {
	Diagnostic {
		code: "QUERY_005".to_string(),
		rql: None,
		message: format!("AS clause body must be a query, found {}", kind),
		fragment,
		label: Some("expected a query expression here".to_string()),
		help: Some(
			"AS { ... } accepts only query expressions (FROM, MAP, FILTER, JOIN, ...); nested DDL statements such as CREATE/DROP/ALTER are not allowed"
				.to_string(),
		),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn not_a_query_input(fragment: Fragment, kind: &str) -> Diagnostic {
	Diagnostic {
		code: "QUERY_010".to_string(),
		rql: None,
		message: format!("{} cannot be the input of a query step", kind),
		fragment,
		label: Some("a query was expected here".to_string()),
		help: Some(format!(
			"a step such as MAP or FILTER reads from a query; run the {} as its own statement",
			kind
		)),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn window_requires_deferred_view(fragment: Fragment) -> Diagnostic {
	Diagnostic {
		code: "QUERY_007".to_string(),
		rql: None,
		message: "window runs only in deferred views, not in a batch query".to_string(),
		fragment,
		label: Some("a batch query cannot run a window".to_string()),
		help: Some(
			"define the window in CREATE DEFERRED VIEW ns::name AS { ... } and read the view with FROM ns::name"
				.to_string(),
		),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn append_requires_deferred_view(fragment: Fragment) -> Diagnostic {
	Diagnostic {
		code: "QUERY_011".to_string(),
		rql: None,
		message: "append runs only in deferred views, not in a batch query".to_string(),
		fragment,
		label: Some("a batch query cannot run an append".to_string()),
		help: Some(
			"define the append in CREATE DEFERRED VIEW ns::name AS { ... } and read the view with FROM ns::name"
				.to_string(),
		),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn no_column_block(fragment: Fragment, kind: &str, name: &str) -> Diagnostic {
	let help = match kind {
		"series" => format!("wait for the bucket to seal, or read the {} with a regular query", kind),
		_ => format!("wait for the column store to materialize the {}, or read it with a regular query", kind),
	};
	Diagnostic {
		code: "QUERY_012".to_string(),
		rql: None,
		message: format!("no column block for {} '{}'", kind, name),
		fragment,
		label: Some(format!("this {} has not been materialized into the column store yet", kind)),
		help: Some(help),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn unsupported_in_column_layout(what: &str) -> Diagnostic {
	Diagnostic {
		code: "QUERY_013".to_string(),
		rql: None,
		message: format!("{} cannot be read from the column store", what),
		fragment: Fragment::None,
		label: Some("a column query reads only materialized table blocks".to_string()),
		help: Some("run this query with a regular query instead".to_string()),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

pub fn unknown_apply_operator(fragment: Fragment) -> Diagnostic {
	let name = fragment.text().to_string();
	Diagnostic {
		code: "QUERY_008".to_string(),
		rql: None,
		message: format!("unknown operator '{}'", name),
		fragment,
		label: Some("no operator with this name is registered".to_string()),
		help: Some("check the operator name for typos, or register the operator before applying it".to_string()),
		column: None,
		notes: vec![],
		cause: None,
		operator_chain: None,
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn memory_limit_exceeded_has_stable_code_and_fields() {
		let d = memory_limit_exceeded(ByteSize::from_kib(5), ByteSize::from_kib(4));
		assert_eq!(d.code, "QUERY_006");
		assert!(matches!(d.fragment, Fragment::None));
		assert!(d.message.contains("exceeded its memory limit of 4 KiB"), "unexpected message: {}", d.message);
		assert!(d.message.contains("needs at least 5 KiB"), "unexpected message: {}", d.message);
		assert!(d.label.unwrap().contains("buffers more data in memory"));
		assert!(d.help.unwrap().contains("raise the query memory limit"));
	}
}