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