Skip to main content

datafusion_ethers/
provider.rs

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///////////////////////////////////////////////////////////////////////////////////////////////////
29// Catalog
30///////////////////////////////////////////////////////////////////////////////////////////////////
31
32#[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///////////////////////////////////////////////////////////////////////////////////////////////////
62// Schema
63///////////////////////////////////////////////////////////////////////////////////////////////////
64
65#[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///////////////////////////////////////////////////////////////////////////////////////////////////
99// Table: eth_logs
100///////////////////////////////////////////////////////////////////////////////////////////////////
101
102#[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 OR expr OR ...
195                (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    /// Converts expressions like:
257    ///   x = v1 or (x = v2 or (x = v3))
258    /// into:
259    ///   [v1, v2, v3]
260    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                // Merge values from two branches, but only if they refer to the same column
271                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        // TODO: Datafusion always passess a projection even when one is not required
340        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        // Get filter with range restrictions from the session config level
360        let mut filter = config.default_filter();
361
362        // Push down filter expressions from the query
363        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///////////////////////////////////////////////////////////////////////////////////////////////////
381
382#[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                    // TODO: Change to Unbounded
415                    datafusion::physical_plan::execution_plan::Boundedness::Bounded,
416                )
417                .with_scheduling_type(
418                    // TODO: Validate assumption: Since we pull data from the network
419                    // we will never get a situation where control is never returned
420                    // to the task scheduler.
421                    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            // TODO: Streaming/unbounded API
449            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                // Don't form batches that are too small
462                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            // Return at least one batch, even if empty
473            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        // this is a leaf node and has no children
496        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                    // Provide deterministic order
559                    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                    // Provide deterministic order
573                    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                    // Provide deterministic order
615                    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                    // Provide deterministic order
629                    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///////////////////////////////////////////////////////////////////////////////////////////////////