reifydb-rql 0.4.12

ReifyDB Query Language (RQL) parser and AST
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2025 ReifyDB

use reifydb_type::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()?;
		// Consume APPEND keyword
		self.advance()?;

		// Check if next token is '{' — if so, this is the query form (APPEND { subquery })
		if !self.is_eof() && self.current()?.is_operator(Operator::OpenCurly) {
			let with = self.parse_sub_query()?;
			return Ok(AstAppend::Query {
				token,
				with,
			});
		}

		// Otherwise, imperative form: APPEND $target FROM <source>
		// Parse target variable ($name)
		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,
		};

		// Consume FROM keyword
		self.consume_keyword(Keyword::From)?;

		// Dispatch on next token
		let source = if !self.is_eof() && self.current()?.is_operator(Operator::OpenBracket) {
			// Inline: APPEND $x FROM [{...}]
			AstAppendSource::Inline(self.parse_list()?)
		} else if !self.is_eof() && matches!(self.current()?.kind, TokenKind::Variable) {
			// Variable source: always treat as frame source, then parse any pipe continuation
			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;

			// Check for pipe continuation (e.g., $data | filter { ... })
			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()?; // consume the pipe
					has_pipes = true;
					nodes.push(self.parse_node(Precedence::None)?);
				} else {
					break;
				}
			}

			AstAppendSource::Statement(AstStatement {
				nodes,
				has_pipes,
				is_output: false,
				rql: "", // Internal statement
			})
		} else {
			// Statement: APPEND $x FROM table | FILTER ...
			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 { .. })));
	}
}