1use alloy_core::primitives::{Address, B256};
2use alloy_network::AnyNetwork;
3use alloy_provider::DynProvider;
4use alloy_rpc_types_eth::{BlockNumberOrTag, Filter, FilterBlockOption};
5use datafusion::catalog::{CatalogProvider, SchemaProvider, Session};
6use datafusion::common::tree_node::TreeNodeRecursion;
7use datafusion::error::{DataFusionError, Result as DfResult};
8use datafusion::execution::TaskContext;
9use datafusion::logical_expr::BinaryExpr;
10use datafusion::physical_expr::EquivalenceProperties;
11use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
12use datafusion::physical_plan::{DisplayAs, DisplayFormatType, PlanProperties};
13use datafusion::{
14 arrow::{array::RecordBatch, datatypes::SchemaRef},
15 datasource::{TableProvider, TableType},
16 logical_expr::{Operator, TableProviderFilterPushDown},
17 physical_plan::ExecutionPlan,
18 prelude::*,
19 scalar::ScalarValue,
20};
21use futures::{Stream, TryStreamExt};
22use std::sync::Arc;
23
24use crate::config::EthProviderConfig;
25use crate::convert::Transcoder as _;
26use crate::stream::StreamOptions;
27use crate::utils::*;
28
29#[derive(Debug)]
34pub struct EthCatalog {
35 config: EthProviderConfig,
36 rpc_client: DynProvider<AnyNetwork>,
37}
38
39impl EthCatalog {
40 pub fn new(config: EthProviderConfig, rpc_client: DynProvider<AnyNetwork>) -> Self {
41 Self { config, rpc_client }
42 }
43}
44
45impl CatalogProvider for EthCatalog {
46 fn schema_names(&self) -> Vec<String> {
47 vec![self.config.schema_name.clone()]
48 }
49
50 fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>> {
51 if name != self.config.schema_name {
52 return None;
53 }
54
55 Some(Arc::new(EthSchema::new(
56 self.config.clone(),
57 self.rpc_client.clone(),
58 )))
59 }
60}
61
62#[derive(Debug)]
67pub struct EthSchema {
68 config: EthProviderConfig,
69 rpc_client: DynProvider<AnyNetwork>,
70}
71
72impl EthSchema {
73 pub fn new(config: EthProviderConfig, rpc_client: DynProvider<AnyNetwork>) -> Self {
74 Self { config, rpc_client }
75 }
76}
77
78#[async_trait::async_trait]
79impl SchemaProvider for EthSchema {
80 fn table_names(&self) -> Vec<String> {
81 vec!["logs".to_string()]
82 }
83
84 async fn table(&self, name: &str) -> DfResult<Option<Arc<dyn TableProvider>>> {
85 match name {
86 "logs" => Ok(Some(Arc::new(EthLogsTable::new(
87 &self.config,
88 self.rpc_client.clone(),
89 )))),
90 _ => Ok(None),
91 }
92 }
93
94 fn table_exist(&self, name: &str) -> bool {
95 name == "logs"
96 }
97}
98
99#[derive(Debug)]
104pub struct EthLogsTable {
105 schema: SchemaRef,
106 rpc_client: DynProvider<AnyNetwork>,
107}
108
109impl EthLogsTable {
110 pub fn new(config: &EthProviderConfig, rpc_client: DynProvider<AnyNetwork>) -> Self {
111 let encoder = crate::convert::EthRawLogsToArrow::new(&config.stream_options());
112 let schema = encoder.schema();
113
114 Self { schema, rpc_client }
115 }
116
117 fn apply_expr(filter: Filter, expr: &Expr) -> (TableProviderFilterPushDown, Filter) {
118 match expr {
119 Expr::BinaryExpr(e) => match (&*e.left, e.op, &*e.right) {
120 (Expr::Column(left), op, right) => match (left.name.as_str(), op, right) {
121 (
122 "address",
123 Operator::Eq,
124 Expr::Literal(ScalarValue::Binary(Some(v)), None),
125 ) => (
126 TableProviderFilterPushDown::Exact,
127 filter.address(Address::from_slice(&v[..])),
128 ),
129 ("topic0", Operator::Eq, Expr::Literal(ScalarValue::Binary(Some(v)), None)) => {
130 (
131 TableProviderFilterPushDown::Exact,
132 filter.event_signature(B256::from_slice(&v[..])),
133 )
134 }
135 ("topic1", Operator::Eq, Expr::Literal(ScalarValue::Binary(Some(v)), None)) => {
136 (
137 TableProviderFilterPushDown::Exact,
138 filter.topic1(B256::from_slice(&v[..])),
139 )
140 }
141 ("topic2", Operator::Eq, Expr::Literal(ScalarValue::Binary(Some(v)), None)) => {
142 (
143 TableProviderFilterPushDown::Exact,
144 filter.topic2(B256::from_slice(&v[..])),
145 )
146 }
147 ("topic3", Operator::Eq, Expr::Literal(ScalarValue::Binary(Some(v)), None)) => {
148 (
149 TableProviderFilterPushDown::Exact,
150 filter.topic3(B256::from_slice(&v[..])),
151 )
152 }
153 (
154 "block_number",
155 Operator::Eq,
156 Expr::Literal(ScalarValue::UInt64(Some(v)), None),
157 ) => (
158 TableProviderFilterPushDown::Exact,
159 filter.union((*v).into(), (*v).into()),
160 ),
161 (
162 "block_number",
163 Operator::Gt,
164 Expr::Literal(ScalarValue::UInt64(Some(v)), None),
165 ) => (
166 TableProviderFilterPushDown::Exact,
167 filter.union((*v + 1).into(), BlockNumberOrTag::Latest),
168 ),
169 (
170 "block_number",
171 Operator::GtEq,
172 Expr::Literal(ScalarValue::UInt64(Some(v)), None),
173 ) => (
174 TableProviderFilterPushDown::Exact,
175 filter.union((*v).into(), BlockNumberOrTag::Latest),
176 ),
177 (
178 "block_number",
179 Operator::Lt,
180 Expr::Literal(ScalarValue::UInt64(Some(v)), None),
181 ) => (
182 TableProviderFilterPushDown::Exact,
183 filter.union(BlockNumberOrTag::Earliest, (*v - 1).into()),
184 ),
185 (
186 "block_number",
187 Operator::LtEq,
188 Expr::Literal(ScalarValue::UInt64(Some(v)), None),
189 ) => (
190 TableProviderFilterPushDown::Exact,
191 filter.union(BlockNumberOrTag::Earliest, (*v).into()),
192 ),
193 _ => (TableProviderFilterPushDown::Unsupported, filter),
194 },
195 (Expr::BinaryExpr(_), Operator::Or, Expr::BinaryExpr(_)) => {
197 match Self::collect_or_group(e) {
198 (TableProviderFilterPushDown::Exact, Some(col), values) => {
199 match col.name.as_str() {
200 "address" => (
201 TableProviderFilterPushDown::Exact,
202 filter.address(
203 values
204 .into_iter()
205 .map(|v| Address::from_slice(&v[..]))
206 .collect::<Vec<_>>(),
207 ),
208 ),
209 "topic0" => (
210 TableProviderFilterPushDown::Exact,
211 filter.event_signature(
212 values
213 .into_iter()
214 .map(|v| B256::from_slice(&v[..]))
215 .collect::<Vec<_>>(),
216 ),
217 ),
218 "topic1" => (
219 TableProviderFilterPushDown::Exact,
220 filter.topic1(
221 values
222 .into_iter()
223 .map(|v| B256::from_slice(&v[..]))
224 .collect::<Vec<_>>(),
225 ),
226 ),
227 "topic2" => (
228 TableProviderFilterPushDown::Exact,
229 filter.topic2(
230 values
231 .into_iter()
232 .map(|v| B256::from_slice(&v[..]))
233 .collect::<Vec<_>>(),
234 ),
235 ),
236 "topic3" => (
237 TableProviderFilterPushDown::Exact,
238 filter.topic3(
239 values
240 .into_iter()
241 .map(|v| B256::from_slice(&v[..]))
242 .collect::<Vec<_>>(),
243 ),
244 ),
245 _ => (TableProviderFilterPushDown::Unsupported, filter),
246 }
247 }
248 _ => (TableProviderFilterPushDown::Unsupported, filter),
249 }
250 }
251 _ => (TableProviderFilterPushDown::Unsupported, filter),
252 },
253 _ => (TableProviderFilterPushDown::Unsupported, filter),
254 }
255 }
256
257 fn collect_or_group(
262 expr: &BinaryExpr,
263 ) -> (TableProviderFilterPushDown, Option<&Column>, Vec<&Vec<u8>>) {
264 match (&*expr.left, expr.op, &*expr.right) {
265 (
266 Expr::Column(col),
267 Operator::Eq,
268 Expr::Literal(ScalarValue::Binary(Some(val)), None),
269 ) => (TableProviderFilterPushDown::Exact, Some(col), vec![val]),
270 (Expr::BinaryExpr(left), Operator::Or, Expr::BinaryExpr(right)) => {
271 let mut left = Self::collect_or_group(left);
273 let mut right = Self::collect_or_group(right);
274 if left.0 == right.0
275 && right.0 == TableProviderFilterPushDown::Exact
276 && left.1 == right.1
277 {
278 left.2.append(&mut right.2);
279 (TableProviderFilterPushDown::Exact, left.1, left.2)
280 } else {
281 (TableProviderFilterPushDown::Unsupported, None, Vec::new())
282 }
283 }
284 _ => (TableProviderFilterPushDown::Unsupported, None, Vec::new()),
285 }
286 }
287}
288
289#[async_trait::async_trait]
290impl TableProvider for EthLogsTable {
291 fn table_type(&self) -> TableType {
292 TableType::Base
293 }
294
295 fn schema(&self) -> SchemaRef {
296 self.schema.clone()
297 }
298
299 fn supports_filters_pushdown(
300 &self,
301 filters: &[&Expr],
302 ) -> datafusion::error::Result<Vec<TableProviderFilterPushDown>> {
303 tracing::debug!(?filters, "EthLogs: performing filter pushdown");
304
305 let mut filter = EthProviderConfig::default().default_filter();
306 let mut res = Vec::new();
307
308 for expr in filters {
309 let (support, new_filter) = Self::apply_expr(filter, expr);
310 filter = new_filter;
311 res.push(support);
312 }
313
314 tracing::debug!(?filter, "EthLogs: resulting filter");
315 Ok(res)
316 }
317
318 async fn scan(
319 &self,
320 state: &dyn Session,
321 mut projection: Option<&Vec<usize>>,
322 filters: &[Expr],
323 limit: Option<usize>,
324 ) -> datafusion::error::Result<Arc<dyn ExecutionPlan>> {
325 let config = state
326 .config_options()
327 .extensions
328 .get::<EthProviderConfig>()
329 .cloned()
330 .unwrap_or_default();
331
332 tracing::debug!(
333 ?config,
334 ?projection,
335 ?filters,
336 ?limit,
337 "EthLogs: creating execution plan"
338 );
339
340 if let Some(proj) = projection {
342 let is_redundant = proj.len() == self.schema.fields.len()
343 && proj[0] == 0
344 && proj
345 .iter()
346 .cloned()
347 .reduce(|a, b| if a + 1 == b { b } else { a })
348 == Some(self.schema.fields.len() - 1);
349 if is_redundant {
350 projection = None
351 }
352 }
353
354 let schema = if let Some(proj) = projection {
355 Arc::new(self.schema.project(proj)?)
356 } else {
357 self.schema.clone()
358 };
359
360 let mut filter = config.default_filter();
362
363 for expr in filters {
365 let (support, new_filter) = Self::apply_expr(filter, expr);
366 assert_eq!(support, TableProviderFilterPushDown::Exact);
367 filter = new_filter;
368 }
369
370 Ok(Arc::new(EthGetLogs::new(
371 self.rpc_client.clone(),
372 schema,
373 projection.cloned(),
374 filter,
375 config.stream_options(),
376 limit,
377 )))
378 }
379}
380
381#[derive(Debug, Clone)]
384pub struct EthGetLogs {
385 projected_schema: SchemaRef,
386 projection: Option<Vec<usize>>,
387 rpc_client: DynProvider<AnyNetwork>,
388 filter: Filter,
389 stream_options: StreamOptions,
390 limit: Option<usize>,
391 properties: Arc<PlanProperties>,
392}
393
394impl EthGetLogs {
395 pub fn new(
396 rpc_client: DynProvider<AnyNetwork>,
397 projected_schema: SchemaRef,
398 projection: Option<Vec<usize>>,
399 filter: Filter,
400 stream_options: StreamOptions,
401 limit: Option<usize>,
402 ) -> Self {
403 Self {
404 projected_schema: projected_schema.clone(),
405 projection,
406 rpc_client,
407 filter,
408 stream_options,
409 limit,
410 properties: Arc::new(
411 PlanProperties::new(
412 EquivalenceProperties::new(projected_schema),
413 datafusion::physical_expr::Partitioning::UnknownPartitioning(1),
414 datafusion::physical_plan::execution_plan::EmissionType::Incremental,
415 datafusion::physical_plan::execution_plan::Boundedness::Bounded,
417 )
418 .with_scheduling_type(
419 datafusion::physical_plan::execution_plan::SchedulingType::Cooperative,
423 ),
424 ),
425 }
426 }
427
428 pub fn filter(&self) -> &Filter {
429 &self.filter
430 }
431
432 pub fn with_filter(mut self, filter: Filter) -> Self {
433 self.filter = filter;
434 self
435 }
436
437 fn execute_impl(
438 rpc_client: DynProvider<AnyNetwork>,
439 filter: Filter,
440 options: StreamOptions,
441 projection: Option<Vec<usize>>,
442 limit: Option<usize>,
443 ) -> impl Stream<Item = DfResult<RecordBatch>> {
444 async_stream::try_stream! {
445 let limit = limit.unwrap_or(usize::MAX);
446 let mut coder = crate::convert::EthRawLogsToArrow::new(&options);
447 let mut total = 0;
448
449 let mut log_stream = Box::pin(
451 crate::stream::RawLogsStream::paginate(rpc_client.clone(), filter, options, None)
452 );
453
454 while let Some(batch) = log_stream.try_next().await.map_err(|e| DataFusionError::External(e.into()))? {
455 if total >= limit {
456 break;
457 }
458 let to_append = usize::min(batch.logs.len(), limit - total);
459 coder.append(&batch.logs[0..to_append]).map_err(|e| DataFusionError::External(e.into()))?;
460 total += to_append;
461
462 if coder.len() > 1_000 {
464 let batch = if let Some(proj) = &projection {
465 coder.finish().project(proj)?
466 } else {
467 coder.finish()
468 };
469 yield batch;
470 }
471 }
472
473 if !coder.is_empty() || total == 0 {
475 let batch = if let Some(proj) = &projection {
476 coder.finish().project(proj)?
477 } else {
478 coder.finish()
479 };
480 yield batch;
481 }
482 }
483 }
484}
485
486impl ExecutionPlan for EthGetLogs {
487 fn name(&self) -> &str {
488 Self::static_name()
489 }
490
491 fn properties(&self) -> &Arc<PlanProperties> {
492 &self.properties
493 }
494
495 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
496 vec![]
498 }
499
500 fn apply_expressions(
501 &self,
502 _f: &mut dyn FnMut(
503 &Arc<dyn datafusion::physical_plan::PhysicalExpr>,
504 ) -> DfResult<TreeNodeRecursion>,
505 ) -> DfResult<TreeNodeRecursion> {
506 Ok(TreeNodeRecursion::Continue)
507 }
508
509 fn with_new_children(
510 self: Arc<Self>,
511 _: Vec<Arc<dyn ExecutionPlan>>,
512 ) -> DfResult<Arc<dyn ExecutionPlan>> {
513 Ok(self)
514 }
515
516 fn execute(
517 &self,
518 partition: usize,
519 _context: Arc<TaskContext>,
520 ) -> DfResult<datafusion::execution::SendableRecordBatchStream> {
521 assert_eq!(partition, 0);
522
523 let stream = Self::execute_impl(
524 self.rpc_client.clone(),
525 self.filter.clone(),
526 self.stream_options.clone(),
527 self.projection.clone(),
528 self.limit,
529 );
530
531 let record_batch_stream =
532 RecordBatchStreamAdapter::new(self.projected_schema.clone(), stream);
533
534 Ok(Box::pin(record_batch_stream))
535 }
536}
537
538impl DisplayAs for EthGetLogs {
539 fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
540 match t {
541 DisplayFormatType::Default | DisplayFormatType::Verbose => {
542 write!(f, "EthGetLogs: ")?;
543 if self.projection.is_none() {
544 write!(f, "projection=[*]")?;
545 } else {
546 let projection_str = self
547 .projected_schema
548 .fields
549 .iter()
550 .map(|f| f.name().as_str())
551 .collect::<Vec<_>>()
552 .join(", ");
553
554 write!(f, "projection=[{projection_str}]")?;
555 }
556
557 let mut filters = Vec::new();
558
559 match self.filter.block_option {
560 FilterBlockOption::Range {
561 from_block,
562 to_block,
563 } => filters.push(format!("block_number=[{from_block:?}, {to_block:?}]")),
564 FilterBlockOption::AtBlockHash(h) => filters.push(format!("block_hash={h}")),
565 }
566
567 if !self.filter.address.is_empty() {
568 let mut addrs: Vec<_> =
570 self.filter.address.iter().map(|h| h.to_string()).collect();
571 addrs.sort();
572 filters.push(format!("address=[{}]", addrs.join(", ")));
573 }
574
575 for (i, t) in self
576 .filter
577 .topics
578 .iter()
579 .enumerate()
580 .filter(|(_, t)| !t.is_empty())
581 {
582 let mut topics: Vec<_> = t.iter().map(|h| h.to_string()).collect();
584 topics.sort();
585 filters.push(format!("topic{}=[{}]", i, topics.join(", ")));
586 }
587
588 write!(f, ", filter=[{}]", filters.join(", "))?;
589
590 if let Some(limit) = self.limit {
591 write!(f, ", limit={limit}")?;
592 }
593
594 Ok(())
595 }
596 DisplayFormatType::TreeRender => {
597 writeln!(f, "EthGetLogs")?;
598
599 if self.projection.is_none() {
600 writeln!(f, "projection=[*]")?;
601 } else {
602 let projection_str = self
603 .projected_schema
604 .fields
605 .iter()
606 .map(|f| f.name().as_str())
607 .collect::<Vec<_>>()
608 .join(", ");
609
610 writeln!(f, "projection=[{projection_str}]")?;
611 }
612
613 let mut filters = Vec::new();
614
615 match self.filter.block_option {
616 FilterBlockOption::Range {
617 from_block,
618 to_block,
619 } => filters.push(format!("block_number=[{from_block:?}, {to_block:?}]")),
620 FilterBlockOption::AtBlockHash(h) => filters.push(format!("block_hash={h}")),
621 }
622
623 if !self.filter.address.is_empty() {
624 let mut addrs: Vec<_> =
626 self.filter.address.iter().map(|h| h.to_string()).collect();
627 addrs.sort();
628 filters.push(format!("address=[{}]", addrs.join(", ")));
629 }
630
631 for (i, t) in self
632 .filter
633 .topics
634 .iter()
635 .enumerate()
636 .filter(|(_, t)| !t.is_empty())
637 {
638 let mut topics: Vec<_> = t.iter().map(|h| h.to_string()).collect();
640 topics.sort();
641 filters.push(format!("topic{}=[{}]", i, topics.join(", ")));
642 }
643
644 writeln!(f, "filter=[{}]", filters.join(", "))?;
645
646 if let Some(limit) = self.limit {
647 writeln!(f, "limit={limit}")?;
648 }
649
650 Ok(())
651 }
652 }
653 }
654}
655
656