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::{any::Any, 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 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///////////////////////////////////////////////////////////////////////////////////////////////////
66// Schema
67///////////////////////////////////////////////////////////////////////////////////////////////////
68
69#[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///////////////////////////////////////////////////////////////////////////////////////////////////
107// Table: eth_logs
108///////////////////////////////////////////////////////////////////////////////////////////////////
109
110#[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 OR expr OR ...
203                (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    /// Converts expressions like:
265    ///   x = v1 or (x = v2 or (x = v3))
266    /// into:
267    ///   [v1, v2, v3]
268    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                // Merge values from two branches, but only if they refer to the same column
279                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        // TODO: Datafusion always passess a projection even when one is not required
352        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        // Get filter with range restrictions from the session config level
372        let mut filter = config.default_filter();
373
374        // Push down filter expressions from the query
375        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///////////////////////////////////////////////////////////////////////////////////////////////////
393
394#[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                    // TODO: Change to Unbounded
427                    datafusion::physical_plan::execution_plan::Boundedness::Bounded,
428                )
429                .with_scheduling_type(
430                    // TODO: Validate assumption: Since we pull data from the network
431                    // we will never get a situation where control is never returned
432                    // to the task scheduler.
433                    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            // TODO: Streaming/unbounded API
461            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                // Don't form batches that are too small
474                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            // Return at least one batch, even if empty
485            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        // this is a leaf node and has no children
512        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                    // Provide deterministic order
575                    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                    // Provide deterministic order
589                    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                    // Provide deterministic order
631                    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                    // Provide deterministic order
645                    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///////////////////////////////////////////////////////////////////////////////////////////////////