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