diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 80f445e..2e6b17f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -58,6 +58,9 @@ impl ResultHandler { ExpressionResult::Log(query_res) => { println!("{}", to_table(query_res)?); } + ExpressionResult::Count(query_res) => { + println!("{}", to_table(query_res)?); + } } } diff --git a/crates/cli/src/repl.rs b/crates/cli/src/repl.rs index e314310..444567a 100644 --- a/crates/cli/src/repl.rs +++ b/crates/cli/src/repl.rs @@ -241,6 +241,12 @@ impl Repl { queue!(stdout(), MoveToNextLine(1), Print(line.cyan())).unwrap(); }); } + ExpressionResult::Count(query_res) => { + let table = to_table(query_res)?; + table.to_string().split("\n").for_each(|line| { + queue!(stdout(), MoveToNextLine(1), Print(line.cyan())).unwrap(); + }); + } } } diff --git a/crates/core/src/common/logs.rs b/crates/core/src/common/logs.rs index 019b115..9af0fc9 100644 --- a/crates/core/src/common/logs.rs +++ b/crates/core/src/common/logs.rs @@ -142,7 +142,8 @@ impl TryFrom> for LogFilter { match pair.as_rule() { Rule::address_filter_type => extract_value(pair, |s| { Ok(LogFilter::EmitterAddress(Address::parse_checksummed( - Address::to_checksum(&Address::from_str(s)?, None), None, + Address::to_checksum(&Address::from_str(s)?, None), + None, )?)) }), Rule::blockrange_filter => parse_block_range(pair), diff --git a/crates/core/src/common/query_result.rs b/crates/core/src/common/query_result.rs index 73d6f06..4308688 100644 --- a/crates/core/src/common/query_result.rs +++ b/crates/core/src/common/query_result.rs @@ -25,6 +25,9 @@ pub enum ExpressionResult { Transaction(Vec), #[serde(rename = "log")] Log(Vec), + #[serde(rename = "count")] + Count(Vec), + } // TODO: should this be replaced with Alloy's Block? @@ -333,6 +336,20 @@ impl Default for LogQueryRes { } } +#[serde_with::skip_serializing_none] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] +pub struct CountQueryRes { + pub count: usize, +} + +impl Default for CountQueryRes { + fn default() -> Self { + Self { + count:0 + } + } +} + fn serialize_option_u256(option: &Option, serializer: S) -> Result where S: Serializer, diff --git a/crates/core/src/common/serializer.rs b/crates/core/src/common/serializer.rs index 92b135c..d1c0c8f 100644 --- a/crates/core/src/common/serializer.rs +++ b/crates/core/src/common/serializer.rs @@ -25,6 +25,7 @@ pub(crate) fn dump_results(result: &ExpressionResult, dump: &Dump) -> Result<(), ExpressionResult::Block(blocks) => serialize_csv(blocks)?, ExpressionResult::Transaction(txs) => serialize_csv(txs)?, ExpressionResult::Log(logs) => serialize_csv(logs)?, + ExpressionResult::Count(count)=> serialize_csv(count)?, }; std::fs::write(dump.path(), content)?; @@ -59,6 +60,7 @@ fn serialize_parquet(result: &ExpressionResult) -> Result, Box create_parquet_schema_and_data(logs)?, + ExpressionResult::Count(count) => create_parquet_schema_and_data(count)?, }; let batch = RecordBatch::try_new(Arc::new(schema), data)?; diff --git a/crates/core/src/common/types.rs b/crates/core/src/common/types.rs index 2ab0980..9c03fd7 100644 --- a/crates/core/src/common/types.rs +++ b/crates/core/src/common/types.rs @@ -10,8 +10,67 @@ use pest::iterators::Pairs; #[derive(Debug, PartialEq)] pub enum Expression { Get(GetExpression), + Count(CountExpression), } +#[derive(Debug, PartialEq)] +pub struct CountExpression { + pub query: GetExpression, +} + +impl CountExpression { + fn new(query: GetExpression) -> Self { + Self { query } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum ExpressionError { + #[error(transparent)] + Count(#[from] CountExpressionError), + + #[error(transparent)] + Get(#[from] GetExpressionError), +} + +#[derive(thiserror::Error, Debug)] +pub enum CountExpressionError { + #[error("Unexpected token: {0}")] + UnexpectedToken(String), +// #[error("Missing entity")] +// MissingEntity, +// #[error("Missing chain or RPC")] +// MissingChainOrRpc, +// #[error("URL parse error: {0}")] +// UrlParseError(String), +// #[error(transparent)] +// EntityError(#[from] EntityError), +// #[error(transparent)] +// ChainError(#[from] ChainError), +// #[error(transparent)] +// DumpError(#[from] DumpError), +} + +impl TryFrom> for CountExpression { + type Error = ExpressionError; + + fn try_from(mut pairs: Pairs<'_, Rule>) -> Result { + let pair = pairs.next().ok_or(CountExpressionError::UnexpectedToken("Expected COUNT expression".into()))?; + + if pair.as_rule() != Rule::count { + return Err(CountExpressionError::UnexpectedToken("Expected COUNT expression".into()).into()); + } + + let mut inner_pairs = pair.into_inner(); + let get_pair = inner_pairs.next().ok_or(CountExpressionError::UnexpectedToken("Expected GET expression inside COUNT".into()))?; + + let get_expression = GetExpression::try_from(get_pair.into_inner()).map_err(ExpressionError::Get)?; + + Ok(CountExpression { query : get_expression }) + } +} + + #[derive(Debug, PartialEq)] pub struct GetExpression { pub entity: Entity, diff --git a/crates/core/src/interpreter/backend/execution_engine.rs b/crates/core/src/interpreter/backend/execution_engine.rs index b699e9f..a776123 100644 --- a/crates/core/src/interpreter/backend/execution_engine.rs +++ b/crates/core/src/interpreter/backend/execution_engine.rs @@ -1,11 +1,12 @@ use super::{ - resolve_account::resolve_account_query, - resolve_block::resolve_block_query, - resolve_logs::resolve_log_query, - resolve_transaction::resolve_transaction_query, + resolve_account::resolve_account_query, resolve_block::resolve_block_query, + resolve_logs::resolve_log_query, resolve_transaction::resolve_transaction_query, }; use crate::common::{ - entity::Entity, query_result::{ExpressionResult, QueryResult}, serializer::dump_results, types::{Expression, GetExpression} + entity::Entity, + query_result::{CountQueryRes, ExpressionResult, QueryResult}, + serializer::dump_results, + types::{CountExpression, Expression, GetExpression}, }; use anyhow::Result; @@ -24,10 +25,7 @@ impl ExecutionEngine { ExecutionEngine } - pub async fn run( - &self, - expressions: Vec, - ) -> Result> { + pub async fn run(&self, expressions: Vec) -> Result> { let mut query_results = vec![]; for expression in expressions { @@ -36,21 +34,31 @@ impl ExecutionEngine { let result = self.run_get_expr(&get_expr).await?; query_results.push(QueryResult::new(result)); } + Expression::Count(count_expr) => { + let result = self.run_count_expr(&count_expr).await?; + query_results.push(QueryResult::new(result)); + } } } + Ok(query_results) } - async fn run_get_expr( - &self, - expr: &GetExpression, - ) -> Result { + async fn run_get_expr(&self, expr: &GetExpression) -> Result { let result = match &expr.entity { - Entity::Block(block) => ExpressionResult::Block(resolve_block_query(block, &expr.chains).await?), - Entity::Account(account) => ExpressionResult::Account(resolve_account_query(account, &expr.chains).await?), - Entity::Transaction(transaction) => ExpressionResult::Transaction(resolve_transaction_query(transaction, &expr.chains).await?), - Entity::Logs(logs) => ExpressionResult::Log(resolve_log_query(logs, &expr.chains).await?), + Entity::Block(block) => { + ExpressionResult::Block(resolve_block_query(block, &expr.chains).await?) + } + Entity::Account(account) => { + ExpressionResult::Account(resolve_account_query(account, &expr.chains).await?) + } + Entity::Transaction(transaction) => ExpressionResult::Transaction( + resolve_transaction_query(transaction, &expr.chains).await?, + ), + Entity::Logs(logs) => { + ExpressionResult::Log(resolve_log_query(logs, &expr.chains).await?) + } }; if let Some(dump) = &expr.dump { @@ -59,6 +67,21 @@ impl ExecutionEngine { Ok(result) } + + async fn run_count_expr(&self, expr: &CountExpression) -> Result { + let query_result = self.run_get_expr(&expr.query).await?; + let count = match query_result { + ExpressionResult::Block(data) => data.len(), + ExpressionResult::Account(data) => data.len(), + ExpressionResult::Transaction(data) => data.len(), + ExpressionResult::Log(data) => data.len(), + ExpressionResult::Count(_) => { + return Err(anyhow::anyhow!("Unexpected Count result from run_get_expr")) + } + }; + let result = ExpressionResult::Count(vec![CountQueryRes{ count }]); + Ok(result) + } } #[cfg(test)] @@ -71,9 +94,9 @@ mod test { dump::{Dump, DumpFormat}, ens::NameOrAddress, logs::{LogField, LogFilter, Logs}, - query_result::{AccountQueryRes, BlockQueryRes, LogQueryRes, TransactionQueryRes}, + query_result::{AccountQueryRes, BlockQueryRes, CountQueryRes, LogQueryRes, TransactionQueryRes}, transaction::{Transaction, TransactionField}, - types::{Expression, GetExpression}, + types::{CountExpression, Expression, GetExpression}, }; use alloy::{ eips::BlockNumberOrTag, @@ -82,6 +105,76 @@ mod test { use pretty_assertions::assert_eq; use std::str::FromStr; + #[tokio::test] + async fn test_count_get_logs(){ + let execution_engine = ExecutionEngine::new(); + let expressions = vec![ + Expression::Count(CountExpression { + query: GetExpression { + entity: Entity::Logs(Logs::new( + vec![ + LogFilter::BlockRange(BlockRange::new( + BlockNumberOrTag::Number(4638757), + Some(BlockNumberOrTag::Number(4638758)), + )), + LogFilter::EmitterAddress(address!( + "dac17f958d2ee523a2206206994597c13d831ec7" + )), + LogFilter::Topic0(b256!( + "cb8241adb0c3fdb35b70c24ce35c5eb0c17af7431c99f827d44a445ca624176a" + )), + ], + LogField::all_variants().to_vec(), + )), + chains: vec![ChainOrRpc::Chain(Chain::Ethereum)], + dump: None, + } + }), + ]; + let execution_result = execution_engine.run(expressions).await; + let expected = vec![CountQueryRes { + count:1 + }]; + match execution_result { + Ok(results) => { + assert_eq!(results[0].result, ExpressionResult::Count(expected)); + } + Err(_) => panic!("Error"), + } + } + + #[tokio::test] + async fn test_count_get_block_fields() { + let execution_engine = ExecutionEngine::new(); + let expressions = vec![ + Expression::Count(CountExpression { + query: GetExpression { + entity: Entity::Block(Block::new( + Some(vec![BlockId::Range(BlockRange::new( + BlockNumberOrTag::Number(1), + None, + ))]), + None, + BlockField::all_variants().to_vec(), + )), + dump: None, + chains: vec![ChainOrRpc::Chain(Chain::Ethereum)], + }})]; + let expected = ExpressionResult::Count(vec![ + CountQueryRes { + count:1 + }, + ]); + let execution_result = execution_engine.run(expressions).await; + + match execution_result { + Ok(results) => { + assert_eq!(results[0].result, expected); + } + Err(_) => panic!("Error"), + } + } + #[tokio::test] async fn test_get_logs() { let execution_engine = ExecutionEngine::new(); @@ -92,9 +185,7 @@ mod test { BlockNumberOrTag::Number(4638757), Some(BlockNumberOrTag::Number(4638758)), )), - LogFilter::EmitterAddress(address!( - "dac17f958d2ee523a2206206994597c13d831ec7" - )), + LogFilter::EmitterAddress(address!("dac17f958d2ee523a2206206994597c13d831ec7")), LogFilter::Topic0(b256!( "cb8241adb0c3fdb35b70c24ce35c5eb0c17af7431c99f827d44a445ca624176a" )), @@ -130,7 +221,6 @@ mod test { removed: Some(false), chain: Some(Chain::Ethereum), }]; - match execution_result { Ok(results) => { assert_eq!(results[0].result, ExpressionResult::Log(expected)); @@ -143,18 +233,14 @@ mod test { async fn test_get_block_fields() { let execution_engine = ExecutionEngine::new(); let expressions = vec![Expression::Get(GetExpression { - entity: Entity::Block( - Block::new( - Some(vec![ - BlockId::Range(BlockRange::new( - BlockNumberOrTag::Number(1), - None, - )), - ]), + entity: Entity::Block(Block::new( + Some(vec![BlockId::Range(BlockRange::new( + BlockNumberOrTag::Number(1), None, - BlockField::all_variants().to_vec(), - ) - ), + ))]), + None, + BlockField::all_variants().to_vec(), + )), dump: None, chains: vec![ChainOrRpc::Chain(Chain::Ethereum)], })]; @@ -205,13 +291,13 @@ mod test { async fn test_get_account_fields_using_invalid_ens() { let execution_engine = ExecutionEngine::new(); let expressions = vec![Expression::Get(GetExpression { - entity: Entity::Account( - Account::new( - Some(vec![NameOrAddress::Name(String::from("thisisinvalid235790123801.eth"))]), - None, - vec![AccountField::Balance], - ) - ), + entity: Entity::Account(Account::new( + Some(vec![NameOrAddress::Name(String::from( + "thisisinvalid235790123801.eth", + ))]), + None, + vec![AccountField::Balance], + )), chains: vec![ChainOrRpc::Chain(Chain::Ethereum)], dump: None, })]; @@ -223,16 +309,14 @@ mod test { async fn test_get_transaction_fields() { let execution_engine = ExecutionEngine::new(); let expressions = vec![Expression::Get(GetExpression { - entity: Entity::Transaction( - Transaction::new( - Some(vec![ - b256!("72546b3ca8ef0dfb85fe66d19645e44cb519858c72fbcad0e1c1699256fed890"), - b256!("72546b3ca8ef0dfb85fe66d19645e44cb519858c72fbcad0e1c1699256fed890") - ]), - None, - TransactionField::all_variants().to_vec(), - ) - ), + entity: Entity::Transaction(Transaction::new( + Some(vec![ + b256!("72546b3ca8ef0dfb85fe66d19645e44cb519858c72fbcad0e1c1699256fed890"), + b256!("72546b3ca8ef0dfb85fe66d19645e44cb519858c72fbcad0e1c1699256fed890"), + ]), + None, + TransactionField::all_variants().to_vec(), + )), chains: vec![ChainOrRpc::Chain(Chain::Ethereum)], dump: None, })]; @@ -285,7 +369,7 @@ mod test { chain: Some(Chain::Ethereum), authorization_list: None, }]) - ]; + ]; let result = execution_engine.run(expressions).await; match result { @@ -300,15 +384,13 @@ mod test { async fn test_get_inexistent_transaction() { let execution_engine = ExecutionEngine::new(); let expressions = vec![Expression::Get(GetExpression { - entity: Entity::Transaction( - Transaction::new( - Some(vec![b256!( - "0000000000000000000000000000000000000000000000000000000000000000" - )]), - None, - TransactionField::all_variants().to_vec(), - ) - ), + entity: Entity::Transaction(Transaction::new( + Some(vec![b256!( + "0000000000000000000000000000000000000000000000000000000000000000" + )]), + None, + TransactionField::all_variants().to_vec(), + )), chains: vec![ChainOrRpc::Chain(Chain::Ethereum)], dump: None, })]; @@ -321,18 +403,11 @@ mod test { async fn test_dump_results() { let execution_engine = ExecutionEngine::new(); let expressions = vec![Expression::Get(GetExpression { - entity: Entity::Block( - Block::new( - Some(vec![ - BlockId::Range(BlockRange::new( - 1.into(), - None, - )) - ]), - None, - vec![BlockField::Timestamp], - ) - ), + entity: Entity::Block(Block::new( + Some(vec![BlockId::Range(BlockRange::new(1.into(), None))]), + None, + vec![BlockField::Timestamp], + )), chains: vec![ChainOrRpc::Chain(Chain::Ethereum)], dump: Some(Dump::new(String::from("test"), DumpFormat::Json)), })]; @@ -382,7 +457,9 @@ mod test { ( Expression::Get(GetExpression { entity: Entity::Account(Account::new( - Some(vec![NameOrAddress::Address(address!("dac17f958d2ee523a2206206994597c13d831ec7"))]), + Some(vec![NameOrAddress::Address(address!( + "dac17f958d2ee523a2206206994597c13d831ec7" + ))]), None, vec![AccountField::Chain], )), @@ -397,7 +474,9 @@ mod test { ( Expression::Get(GetExpression { entity: Entity::Transaction(Transaction::new( - Some(vec![b256!("72546b3ca8ef0dfb85fe66d19645e44cb519858c72fbcad0e1c1699256fed890")]), + Some(vec![b256!( + "72546b3ca8ef0dfb85fe66d19645e44cb519858c72fbcad0e1c1699256fed890" + )]), None, vec![TransactionField::Chain], )), diff --git a/crates/core/src/interpreter/backend/mod.rs b/crates/core/src/interpreter/backend/mod.rs index 4f02a10..f05542b 100644 --- a/crates/core/src/interpreter/backend/mod.rs +++ b/crates/core/src/interpreter/backend/mod.rs @@ -1,5 +1,5 @@ +pub mod execution_engine; mod resolve_account; mod resolve_block; mod resolve_logs; mod resolve_transaction; -pub mod execution_engine; diff --git a/crates/core/src/interpreter/frontend/parser.rs b/crates/core/src/interpreter/frontend/parser.rs index caa8195..5f5bdf4 100644 --- a/crates/core/src/interpreter/frontend/parser.rs +++ b/crates/core/src/interpreter/frontend/parser.rs @@ -33,6 +33,9 @@ impl<'a> Parser<'a> { Rule::get => { expressions.push(Expression::Get(pair.into_inner().try_into()?)); } + Rule::count => { + expressions.push(Expression::Count(pair.into_inner().try_into()?)); + } _ => { return Err(ParserError::UnexpectedToken(pair.as_str().to_string()).into()); } diff --git a/crates/core/src/interpreter/frontend/productions.pest b/crates/core/src/interpreter/frontend/productions.pest index d7c8924..b725f09 100644 --- a/crates/core/src/interpreter/frontend/productions.pest +++ b/crates/core/src/interpreter/frontend/productions.pest @@ -1,4 +1,16 @@ -program = _{SOI ~ (get){1, } ~ silent_eoi} +program = _{SOI ~ (get|count){1, } ~ silent_eoi} + +count = { + "COUNT" ~ + WHITESPACE* ~ + "(" ~ + WHITESPACE* ~ + query ~ + WHITESPACE* ~ + ")" +} + +query = { get } get = { entity ~ @@ -13,6 +25,7 @@ get = { entity = { account_get | block_get | tx_get | log_get } + account_get = { "GET" ~ WHITESPACE* ~ diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 41eefed..30fa6ef 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -27,4 +27,4 @@ pub fn enum_variants_derive(input: TokenStream) -> TokenStream { }; TokenStream::from(expanded) -} \ No newline at end of file +}