reifydb-rql 0.6.0

ReifyDB Query Language (RQL) parser and AST
Documentation
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) 2026 ReifyDB

use reifydb_value::error::{AstErrorKind, Error, TypeError};

use super::{Parser, Precedence};
use crate::{
	Result,
	ast::ast::{Ast, AstAppend, AstAppendSource, AstFrom, AstStatement, AstVariable},
	token::{keyword::Keyword, operator::Operator, separator::Separator, token::TokenKind},
};

impl<'bump> Parser<'bump> {
	pub(crate) fn parse_append(&mut self) -> Result<AstAppend<'bump>> {
		let token = self.current()?;

		self.advance()?;

		if !self.is_eof() && self.current()?.is_operator(Operator::OpenCurly) {
			let with = self.parse_sub_query()?;
			let ttl = self.parse_with_clause_for_operator()?;
			return Ok(AstAppend::Query {
				token,
				with,
				ttl,
			});
		}

		let variable_token = self.current()?;
		if !matches!(variable_token.kind, TokenKind::Variable) {
			let fragment = variable_token.fragment.to_owned();
			return Err(Error::from(TypeError::Ast {
				kind: AstErrorKind::UnexpectedToken {
					expected: "expected variable name starting with '$'".to_string(),
				},
				message: format!(
					"Unexpected token: expected {}, got {}",
					"expected variable name starting with '$'",
					fragment.text()
				),
				fragment,
			}));
		}
		let var_token = self.advance()?;
		let target = AstVariable {
			token: var_token,
		};

		self.consume_keyword(Keyword::From)?;

		let source = if !self.is_eof() && self.current()?.is_operator(Operator::OpenBracket) {
			AstAppendSource::Inline(self.parse_list()?)
		} else if !self.is_eof() && matches!(self.current()?.kind, TokenKind::Variable) {
			let src_token = self.advance()?;
			let variable = AstVariable {
				token: src_token,
			};
			let first_node = Ast::From(AstFrom::Variable {
				token: src_token,
				variable,
			});

			let mut nodes = vec![first_node];
			let mut has_pipes = false;

			while !self.is_eof() {
				if let Ok(current) = self.current()
					&& current.is_separator(Separator::Semicolon)
				{
					break;
				}
				if self.current()?.is_operator(Operator::Pipe) {
					self.advance()?;
					has_pipes = true;
					nodes.push(self.parse_node(Precedence::None)?);
				} else {
					break;
				}
			}

			AstAppendSource::Statement(AstStatement {
				nodes,
				has_pipes,
				is_output: false,
				rql: "",
			})
		} else {
			let statement = self.parse_statement_content()?;
			AstAppendSource::Statement(statement)
		};

		Ok(AstAppend::IntoVariable {
			token,
			target,
			source,
		})
	}
}

#[cfg(test)]
pub mod tests {
	use crate::{
		ast::{
			ast::{Ast, AstAppend, AstFrom},
			parse::Parser,
		},
		bump::Bump,
		token::tokenize,
	};

	#[test]
	fn test_append_query_basic() {
		let bump = Bump::new();
		let source = "append { from test::orders }";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let mut parser = Parser::new(&bump, source, tokens);
		let mut result = parser.parse().unwrap();
		assert_eq!(result.len(), 1);

		let result = result.pop().unwrap();
		let node = result.first_unchecked();
		if let Ast::Append(AstAppend::Query {
			with,
			..
		}) = node
		{
			let first_node = with.statement.nodes.first().expect("Expected node in subquery");
			if let Ast::From(AstFrom::Source {
				source,
				..
			}) = first_node
			{
				assert_eq!(source.namespace[0].text(), "test");
				assert_eq!(source.name.text(), "orders");
			} else {
				panic!("Expected From node in subquery");
			}
		} else {
			panic!("Expected Append::Query");
		}
	}

	#[test]
	fn test_append_query_with_from() {
		let bump = Bump::new();
		let source = "from test::source1 append { from test::source2 }";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let mut parser = Parser::new(&bump, source, tokens);
		let result = parser.parse().unwrap();
		assert_eq!(result.len(), 1);

		let statement = &result[0];
		assert_eq!(statement.nodes.len(), 2);

		// First should be FROM
		assert!(statement.nodes[0].is_from());

		// Second should be APPEND Query
		if let Ast::Append(AstAppend::Query {
			with,
			..
		}) = &statement.nodes[1]
		{
			let first_node = with.statement.nodes.first().expect("Expected node in subquery");
			if let Ast::From(AstFrom::Source {
				source,
				..
			}) = first_node
			{
				assert_eq!(source.namespace[0].text(), "test");
				assert_eq!(source.name.text(), "source2");
			} else {
				panic!("Expected From node in subquery");
			}
		} else {
			panic!("Expected Append::Query");
		}
	}

	#[test]
	fn test_append_query_chained() {
		let bump = Bump::new();
		let source = "from test::source1 append { from test::source2 } append { from test::source3 }";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let mut parser = Parser::new(&bump, source, tokens);
		let result = parser.parse().unwrap();
		assert_eq!(result.len(), 1);

		let statement = &result[0];
		assert_eq!(statement.nodes.len(), 3);

		assert!(statement.nodes[0].is_from());
		assert!(matches!(statement.nodes[1], Ast::Append(AstAppend::Query { .. })));
		assert!(matches!(statement.nodes[2], Ast::Append(AstAppend::Query { .. })));
	}

	#[test]
	fn test_append_query_without_ttl() {
		let bump = Bump::new();
		let source = "append { from test::orders }";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let mut parser = Parser::new(&bump, source, tokens);
		let mut result = parser.parse().unwrap();

		let result = result.pop().unwrap();
		let node = result.first_unchecked();
		let Ast::Append(AstAppend::Query {
			ttl,
			..
		}) = node
		else {
			panic!("Expected Append::Query");
		};
		assert!(ttl.is_none(), "no ttl when with-clause is absent");
	}

	#[test]
	fn test_append_query_with_ttl_duration_only() {
		let bump = Bump::new();
		let source = "append { from test::orders } with { ttl: { duration: '1h' } }";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let mut parser = Parser::new(&bump, source, tokens);
		let mut result = parser.parse().unwrap();

		let result = result.pop().unwrap();
		let node = result.first_unchecked();
		let Ast::Append(AstAppend::Query {
			ttl,
			..
		}) = node
		else {
			panic!("Expected Append::Query");
		};
		let ttl = ttl.as_ref().expect("expected ttl");
		assert_eq!(ttl.duration.fragment.text(), "1h");
		assert!(ttl.anchor.is_none());
		assert!(ttl.mode.is_none());
	}

	#[test]
	fn test_append_query_with_ttl_full_config() {
		let bump = Bump::new();
		let source = "append { from test::orders } with { ttl: { duration: '30m', on: updated, mode: drop } }";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let mut parser = Parser::new(&bump, source, tokens);
		let mut result = parser.parse().unwrap();

		let result = result.pop().unwrap();
		let node = result.first_unchecked();
		let Ast::Append(AstAppend::Query {
			ttl,
			..
		}) = node
		else {
			panic!("Expected Append::Query");
		};
		let ttl = ttl.as_ref().expect("expected ttl");
		assert_eq!(ttl.duration.fragment.text(), "30m");
		assert_eq!(ttl.anchor.as_ref().unwrap().fragment.text(), "updated");
		assert_eq!(ttl.mode.as_ref().unwrap().fragment.text(), "drop");
	}

	#[test]
	fn test_append_query_with_unknown_key_rejected() {
		let bump = Bump::new();
		let source = "append { from test::orders } with { unknown: 'foo' }";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let mut parser = Parser::new(&bump, source, tokens);
		let result = parser.parse();
		assert!(result.is_err(), "append should reject unknown WITH keys");
	}
}