1#[cfg(feature = "iejoin")]
2use polars::prelude::JoinTypeOptionsIR;
3use polars::prelude::deletion::DeletionFilesList;
4use polars::prelude::python_dsl::PythonScanSource;
5use polars::prelude::{ColumnMapping, PredicateFileSkip};
6use polars_core::prelude::IdxSize;
7use polars_io::cloud::CloudOptions;
8#[cfg(feature = "asof_join")]
9use polars_ops::prelude::AsofStrategy;
10use polars_ops::prelude::JoinType;
11use polars_plan::plans::{HintIR, IR};
12use polars_plan::prelude::{FileScanIR, FunctionIR, PythonPredicate, UnifiedScanArgs};
13use pyo3::IntoPyObjectExt;
14use pyo3::exceptions::{PyNotImplementedError, PyValueError};
15use pyo3::prelude::*;
16use pyo3::types::{PyDict, PyList, PyString};
17
18use super::expr_nodes::PyGroupbyOptions;
19use crate::PyDataFrame;
20use crate::lazyframe::visit::PyExprIR;
21
22fn scan_type_to_pyobject(
23 py: Python<'_>,
24 scan_type: &FileScanIR,
25 cloud_options: &Option<CloudOptions>,
26) -> PyResult<Py<PyAny>> {
27 match scan_type {
28 #[cfg(feature = "csv")]
29 FileScanIR::Csv { options } => {
30 let options = serde_json::to_string(options)
31 .map_err(|err| PyValueError::new_err(format!("{err:?}")))?;
32 let cloud_options = serde_json::to_string(cloud_options)
33 .map_err(|err| PyValueError::new_err(format!("{err:?}")))?;
34 Ok(("csv", options, cloud_options).into_py_any(py)?)
35 },
36 #[cfg(feature = "parquet")]
37 FileScanIR::Parquet { options, .. } => {
38 let options = serde_json::to_string(options)
39 .map_err(|err| PyValueError::new_err(format!("{err:?}")))?;
40 let cloud_options = serde_json::to_string(cloud_options)
41 .map_err(|err| PyValueError::new_err(format!("{err:?}")))?;
42 Ok(("parquet", options, cloud_options).into_py_any(py)?)
43 },
44 #[cfg(feature = "ipc")]
45 FileScanIR::Ipc { .. } => Err(PyNotImplementedError::new_err("ipc scan")),
46 #[cfg(feature = "json")]
47 FileScanIR::NDJson { options, .. } => {
48 let options = serde_json::to_string(options)
49 .map_err(|err| PyValueError::new_err(format!("{err:?}")))?;
50 Ok(("ndjson", options).into_py_any(py)?)
51 },
52 #[cfg(feature = "scan_lines")]
53 FileScanIR::Lines { name } => Ok(("lines", name.as_str()).into_py_any(py)?),
54 FileScanIR::ExpandedPaths { name } => {
55 Ok(("expanded-paths", name.as_str()).into_py_any(py)?)
56 },
57 FileScanIR::PythonDataset { .. } => {
58 Err(PyNotImplementedError::new_err("python dataset scan"))
59 },
60 FileScanIR::Anonymous { .. } => Err(PyNotImplementedError::new_err("anonymous scan")),
61 }
62}
63
64#[pyclass(frozen)]
65pub struct PythonScan {
67 #[pyo3(get)]
68 options: Py<PyAny>,
69}
70
71#[pyclass(frozen)]
72pub struct Slice {
74 #[pyo3(get)]
75 input: usize,
76 #[pyo3(get)]
77 offset: i64,
78 #[pyo3(get)]
79 len: IdxSize,
80}
81
82#[pyclass(frozen)]
83pub struct Filter {
85 #[pyo3(get)]
86 input: usize,
87 #[pyo3(get)]
88 predicate: PyExprIR,
89}
90
91#[pyclass(frozen, skip_from_py_object)]
92#[derive(Clone)]
93pub struct PyFileOptions {
94 inner: UnifiedScanArgs,
95}
96
97#[pymethods]
98impl PyFileOptions {
99 #[getter]
100 fn n_rows(&self) -> Option<(i64, IdxSize)> {
101 self.inner
102 .pre_slice
103 .clone()
104 .map(|slice| slice.to_signed_offset_len())
105 }
106 #[getter]
107 fn with_columns(&self) -> Option<Vec<&str>> {
108 self.inner
109 .projection
110 .as_ref()?
111 .iter()
112 .map(|x| x.as_str())
113 .collect::<Vec<_>>()
114 .into()
115 }
116 #[getter]
117 fn cache(&self, _py: Python<'_>) -> bool {
118 self.inner.cache
119 }
120 #[getter]
121 fn row_index(&self) -> Option<(&str, IdxSize)> {
122 self.inner
123 .row_index
124 .as_ref()
125 .map(|n| (n.name.as_str(), n.offset))
126 }
127 #[getter]
128 fn rechunk(&self, _py: Python<'_>) -> bool {
129 self.inner.rechunk
130 }
131 #[getter]
132 fn hive_options(&self, _py: Python<'_>) -> PyResult<Py<PyAny>> {
133 Err(PyNotImplementedError::new_err("hive options"))
134 }
135 #[getter]
136 fn include_file_paths(&self, _py: Python<'_>) -> Option<&str> {
137 self.inner.include_file_paths.as_deref()
138 }
139
140 #[getter]
144 fn deletion_files(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
145 Ok(match &self.inner.deletion_files {
146 None => py.None().into_any(),
147 Some(DeletionFilesList::IcebergPositionDelete(paths)) => {
148 let out = PyDict::new(py);
149 for (k, v) in paths.iter() {
150 out.set_item(*k, v.as_ref())?;
151 }
152 ("iceberg-position-delete", out)
153 .into_pyobject(py)?
154 .into_any()
155 .unbind()
156 },
157 Some(DeletionFilesList::Delta(provider)) => {
158 ("delta-deletion-vector", provider.callback().0.clone_ref(py))
159 .into_pyobject(py)?
160 .into_any()
161 .unbind()
162 },
163 })
164 }
165
166 #[getter]
170 fn column_mapping(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
171 Ok(match &self.inner.column_mapping {
172 None => py.None().into_any(),
173
174 Some(ColumnMapping::Iceberg { .. }) => unimplemented!(),
175 })
176 }
177}
178
179#[pyclass(frozen)]
180pub struct Scan {
182 #[pyo3(get)]
183 paths: Py<PyAny>,
184 #[pyo3(get)]
185 file_info: Py<PyAny>,
186 #[pyo3(get)]
187 hive_parts: Option<PyDataFrame>,
188 #[pyo3(get)]
189 predicate: Option<PyExprIR>,
190 #[pyo3(get)]
191 file_options: PyFileOptions,
192 #[pyo3(get)]
193 scan_type: Py<PyAny>,
194}
195
196#[pyclass(frozen)]
197pub struct DataFrameScan {
199 #[pyo3(get)]
200 df: PyDataFrame,
201 #[pyo3(get)]
202 projection: Py<PyAny>,
203 #[pyo3(get)]
204 selection: Option<PyExprIR>,
205}
206
207#[pyclass(frozen)]
208pub struct SimpleProjection {
210 #[pyo3(get)]
211 input: usize,
212}
213
214#[pyclass(frozen)]
215pub struct Select {
217 #[pyo3(get)]
218 input: usize,
219 #[pyo3(get)]
220 expr: Vec<PyExprIR>,
221 #[pyo3(get)]
222 should_broadcast: bool,
223}
224
225#[pyclass(frozen)]
226pub struct Sort {
228 #[pyo3(get)]
229 input: usize,
230 #[pyo3(get)]
231 by_column: Vec<PyExprIR>,
232 #[pyo3(get)]
233 sort_options: (bool, Vec<bool>, Vec<bool>),
234 #[pyo3(get)]
235 slice: Option<(i64, usize, Option<u128>)>,
236}
237
238#[pyclass(frozen)]
239pub struct Cache {
241 #[pyo3(get)]
242 input: usize,
243 #[pyo3(get)]
244 id_: u128,
245}
246
247#[pyclass(frozen)]
248pub struct GroupBy {
250 #[pyo3(get)]
251 input: usize,
252 #[pyo3(get)]
253 keys: Vec<PyExprIR>,
254 #[pyo3(get)]
255 aggs: Vec<PyExprIR>,
256 #[pyo3(get)]
257 apply: (),
258 #[pyo3(get)]
259 maintain_order: bool,
260 #[pyo3(get)]
261 options: Py<PyAny>,
262}
263
264#[pyclass(frozen)]
265pub struct Join {
267 #[pyo3(get)]
268 input_left: usize,
269 #[pyo3(get)]
270 input_right: usize,
271 #[pyo3(get)]
272 left_on: Vec<PyExprIR>,
273 #[pyo3(get)]
274 right_on: Vec<PyExprIR>,
275 #[pyo3(get)]
276 options: Py<PyAny>,
277}
278
279#[pyclass(frozen)]
280pub struct Gather {
282 #[pyo3(get)]
283 input: usize,
284 #[pyo3(get)]
285 idxs: usize,
286 #[pyo3(get)]
287 null_on_oob: bool,
288}
289
290#[pyclass(frozen)]
291pub struct MergeSorted {
293 #[pyo3(get)]
294 input_left: usize,
295 #[pyo3(get)]
296 input_right: usize,
297 #[pyo3(get)]
298 key: Vec<String>,
299 #[pyo3(get)]
300 maintain_order: bool,
301}
302
303#[pyclass(frozen)]
304pub struct HStack {
306 #[pyo3(get)]
307 input: usize,
308 #[pyo3(get)]
309 exprs: Vec<PyExprIR>,
310 #[pyo3(get)]
311 should_broadcast: bool,
312}
313
314#[pyclass(frozen)]
315pub struct Reduce {
317 #[pyo3(get)]
318 input: usize,
319 #[pyo3(get)]
320 exprs: Vec<PyExprIR>,
321}
322
323#[pyclass(frozen)]
324pub struct Distinct {
326 #[pyo3(get)]
327 input: usize,
328 #[pyo3(get)]
329 options: Py<PyAny>,
330}
331#[pyclass(frozen)]
332pub struct MapFunction {
334 #[pyo3(get)]
335 input: usize,
336 #[pyo3(get)]
337 function: Py<PyAny>,
338}
339#[pyclass(frozen)]
340pub struct Union {
341 #[pyo3(get)]
342 inputs: Vec<usize>,
343 #[pyo3(get)]
344 slice: Option<(i64, usize)>,
345 #[pyo3(get)]
346 rows: (Option<usize>, usize),
347 #[pyo3(get)]
348 maintain_order: bool,
349}
350#[pyclass(frozen)]
351pub struct HConcat {
353 #[pyo3(get)]
354 inputs: Vec<usize>,
355 #[pyo3(get)]
356 options: Py<PyAny>,
357}
358#[pyclass(frozen)]
359pub struct ExtContext {
361 #[pyo3(get)]
362 input: usize,
363 #[pyo3(get)]
364 contexts: Vec<usize>,
365}
366
367#[pyclass(frozen)]
368pub struct Sink {
369 #[pyo3(get)]
370 input: usize,
371 #[pyo3(get)]
372 payload: Py<PyAny>,
373}
374
375pub(crate) fn into_py(py: Python<'_>, plan: &IR) -> PyResult<Py<PyAny>> {
376 match plan {
377 IR::PythonScan { options } => {
378 let python_src = match options.python_source {
379 PythonScanSource::Pyarrow => "pyarrow",
380 PythonScanSource::Cuda => "cuda",
381 PythonScanSource::IOPlugin => "io_plugin",
382 };
383
384 PythonScan {
385 options: (
386 options
387 .scan_fn
388 .as_ref()
389 .map_or_else(|| py.None(), |s| s.0.clone_ref(py)),
390 options.with_columns.as_ref().map_or_else(
391 || Ok(py.None()),
392 |cols| {
393 cols.iter()
394 .map(|x| x.as_str())
395 .collect::<Vec<_>>()
396 .into_py_any(py)
397 },
398 )?,
399 python_src,
400 match &options.predicate {
401 PythonPredicate::None => py.None(),
402 PythonPredicate::PyArrow(p) => (
403 "pyarrow",
404 format!("{:?}", p),
405 "has_residual",
406 p.has_residual,
407 )
408 .into_py_any(py)?,
409 PythonPredicate::Polars(e) => ("polars", e.node().0).into_py_any(py)?,
410 },
411 options
412 .n_rows
413 .map_or_else(|| Ok(py.None()), |s| s.into_py_any(py))?,
414 )
415 .into_py_any(py)?,
416 }
417 .into_py_any(py)
418 },
419 IR::Slice { input, offset, len } => Slice {
420 input: input.0,
421 offset: *offset,
422 len: *len,
423 }
424 .into_py_any(py),
425 IR::Filter { input, predicate } => Filter {
426 input: input.0,
427 predicate: predicate.into(),
428 }
429 .into_py_any(py),
430 IR::Scan {
431 sources,
432 file_info: _,
433 hive_parts,
434 predicate,
435 predicate_file_skip_applied,
436 output_schema: _,
437 scan_type,
438 unified_scan_args,
439 } => {
440 Scan {
441 paths: {
442 let paths = sources
443 .into_paths()
444 .ok_or_else(|| PyNotImplementedError::new_err("scan with BytesIO"))?;
445
446 let out = PyList::new(py, [] as [(); 0])?;
447
448 for path in paths.iter() {
451 out.append(path.as_str())?;
452 }
453
454 out.into_py_any(py)?
455 },
456 file_info: py.None(),
458 hive_parts: hive_parts
459 .as_ref()
460 .map(|h| PyDataFrame::new(h.df().clone())),
461 predicate: predicate
462 .as_ref()
463 .filter(|_| {
464 !matches!(
465 predicate_file_skip_applied,
466 Some(PredicateFileSkip {
467 no_residual_predicate: true,
468 original_len: _,
469 })
470 )
471 })
472 .map(|e| e.into()),
473 file_options: PyFileOptions {
474 inner: (**unified_scan_args).clone(),
475 },
476 scan_type: scan_type_to_pyobject(py, scan_type, &unified_scan_args.cloud_options)?,
477 }
478 }
479 .into_py_any(py),
480 IR::DataFrameScan {
481 df,
482 schema: _,
483 output_schema,
484 } => DataFrameScan {
485 df: PyDataFrame::new((**df).clone()),
486 projection: output_schema.as_ref().map_or_else(
487 || Ok(py.None()),
488 |s| {
489 s.iter_names()
490 .map(|s| s.as_str())
491 .collect::<Vec<_>>()
492 .into_py_any(py)
493 },
494 )?,
495 selection: None,
496 }
497 .into_py_any(py),
498 IR::SimpleProjection { input, columns: _ } => {
499 SimpleProjection { input: input.0 }.into_py_any(py)
500 },
501 IR::Select {
502 input,
503 expr,
504 schema: _,
505 options,
506 } => Select {
507 expr: expr.iter().map(|e| e.into()).collect(),
508 input: input.0,
509 should_broadcast: options.should_broadcast,
510 }
511 .into_py_any(py),
512 IR::Sort {
513 input,
514 by_column,
515 slice,
516 sort_options,
517 } => Sort {
518 input: input.0,
519 by_column: by_column.iter().map(|e| e.into()).collect(),
520 sort_options: (
521 sort_options.maintain_order,
522 sort_options.nulls_last.clone(),
523 sort_options.descending.clone(),
524 ),
525 slice: slice
526 .as_ref()
527 .map(|t| (t.0, t.1, t.2.as_ref().map(|p| p.id().as_u128()))),
528 }
529 .into_py_any(py),
530 IR::Cache { input, id } => Cache {
531 input: input.0,
532 id_: id.as_u128(),
533 }
534 .into_py_any(py),
535 IR::GroupBy {
536 input,
537 keys,
538 aggs,
539 schema: _,
540 apply,
541 maintain_order,
542 options,
543 } => GroupBy {
544 input: input.0,
545 keys: keys.iter().map(|e| e.into()).collect(),
546 aggs: aggs.iter().map(|e| e.into()).collect(),
547 apply: apply.as_ref().map_or(Ok(()), |_| {
548 Err(PyNotImplementedError::new_err(format!(
549 "apply inside GroupBy {plan:?}"
550 )))
551 })?,
552 maintain_order: *maintain_order,
553 options: PyGroupbyOptions::new(options.as_ref().clone()).into_py_any(py)?,
554 }
555 .into_py_any(py),
556 IR::Join {
557 input_left,
558 input_right,
559 schema: _,
560 left_on,
561 right_on,
562 options,
563 } => {
564 Join {
565 input_left: input_left.0,
566 input_right: input_right.0,
567 left_on: left_on.iter().map(|e| e.into()).collect(),
568 right_on: right_on.iter().map(|e| e.into()).collect(),
569 options: {
570 let how = &options.args.how;
571 let name = Into::<&str>::into(how).into_pyobject(py)?;
572 (
573 match how {
574 #[cfg(feature = "asof_join")]
575 JoinType::AsOf(asof_options) => {
576 let strategy = match asof_options.strategy {
577 AsofStrategy::Backward => "backward",
578 AsofStrategy::Forward => "forward",
579 AsofStrategy::Nearest => "nearest",
580 };
581 let left_by = asof_options.left_by.as_ref().map(|cols| {
582 cols.iter().map(|c| c.as_str()).collect::<Vec<_>>()
583 });
584 let right_by = asof_options.right_by.as_ref().map(|cols| {
585 cols.iter().map(|c| c.as_str()).collect::<Vec<_>>()
586 });
587 (
588 name,
589 strategy,
590 asof_options.tolerance.as_ref().map_or_else(
591 || Ok(py.None()),
592 |t| crate::Wrap(t.as_any_value()).into_py_any(py),
593 )?,
594 asof_options.tolerance_str.as_ref().map(|s| s.as_str()),
595 left_by,
596 right_by,
597 asof_options.allow_eq,
598 asof_options.check_sortedness,
599 )
600 .into_py_any(py)?
601 },
602 #[cfg(feature = "iejoin")]
603 JoinType::IEJoin => {
604 let Some(JoinTypeOptionsIR::IEJoin(ie_options)) = &options.options
605 else {
606 unreachable!()
607 };
608 (
609 name,
610 crate::Wrap(ie_options.operator1).into_py_any(py)?,
611 ie_options.operator2.as_ref().map_or_else(
612 || Ok(py.None()),
613 |op| crate::Wrap(*op).into_py_any(py),
614 )?,
615 )
616 .into_py_any(py)?
617 },
618 JoinType::Cross if options.options.is_some() => {
621 return Err(PyNotImplementedError::new_err("nested loop join"));
622 },
623 _ => name.into_any().unbind(),
624 },
625 options.args.nulls_equal,
626 options.args.slice,
627 options.args.suffix().as_str(),
628 options.args.coalesce.coalesce(how),
629 Into::<&str>::into(options.args.maintain_order),
630 )
631 .into_py_any(py)?
632 },
633 }
634 .into_py_any(py)
635 },
636 IR::Gather {
637 input,
638 idxs,
639 null_on_oob,
640 } => Gather {
641 input: input.0,
642 idxs: idxs.0,
643 null_on_oob: *null_on_oob,
644 }
645 .into_py_any(py),
646 IR::HStack {
647 input,
648 exprs,
649 schema: _,
650 options,
651 } => HStack {
652 input: input.0,
653 exprs: exprs.iter().map(|e| e.into()).collect(),
654 should_broadcast: options.should_broadcast,
655 }
656 .into_py_any(py),
657 IR::Distinct { input, options } => Distinct {
658 input: input.0,
659 options: (
660 Into::<&str>::into(options.keep_strategy),
661 options.subset.as_ref().map_or_else(
662 || Ok(py.None()),
663 |f| {
664 f.iter()
665 .map(|s| s.as_ref())
666 .collect::<Vec<&str>>()
667 .into_py_any(py)
668 },
669 )?,
670 options.maintain_order,
671 options.slice,
672 )
673 .into_py_any(py)?,
674 }
675 .into_py_any(py),
676 IR::MapFunction { input, function } => MapFunction {
677 input: input.0,
678 function: match function {
679 FunctionIR::OpaquePython(_) => {
680 return Err(PyNotImplementedError::new_err("opaque python mapfunction"));
681 },
682 FunctionIR::Opaque {
683 function: _,
684 schema: _,
685 predicate_pd: _,
686 projection_pd: _,
687 streamable: _,
688 fmt_str: _,
689 } => return Err(PyNotImplementedError::new_err("opaque rust mapfunction")),
690 FunctionIR::Unnest { columns, separator } => (
691 "unnest",
692 columns.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
693 separator.as_ref().map(|s| s.to_string()),
694 )
695 .into_py_any(py)?,
696 FunctionIR::Rechunk => ("rechunk",).into_py_any(py)?,
697 FunctionIR::Explode {
698 columns,
699 options,
700 schema: _,
701 } => (
702 "explode",
703 columns.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
704 options.empty_as_null,
705 options.keep_nulls,
706 )
707 .into_py_any(py)?,
708 #[cfg(feature = "pivot")]
709 FunctionIR::Unpivot { args, schema: _ } => (
710 "unpivot",
711 args.index.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
712 args.on.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
713 args.variable_name.as_str().into_py_any(py)?,
714 args.value_name.as_str().into_py_any(py)?,
715 )
716 .into_py_any(py)?,
717 FunctionIR::RowIndex {
718 name,
719 schema: _,
720 offset,
721 } => ("row_index", name.to_string(), offset.unwrap_or(0)).into_py_any(py)?,
722 FunctionIR::FastCount {
723 sources,
724 scan_type,
725 alias,
726 ..
727 } => {
728 let sources = sources
729 .into_paths()
730 .ok_or_else(|| {
731 PyNotImplementedError::new_err("FastCount with BytesIO sources")
732 })?
733 .iter()
734 .map(|p| p.as_str())
735 .collect::<Vec<_>>()
736 .into_py_any(py)?;
737
738 let cloud_options = None;
740
741 let scan_type = scan_type_to_pyobject(py, scan_type, &cloud_options)?;
742
743 let alias = alias
744 .as_ref()
745 .map(|a| a.as_str())
746 .map_or_else(|| Ok(py.None()), |s| s.into_py_any(py))?;
747
748 ("fast_count", sources, scan_type, alias).into_py_any(py)?
749 },
750 FunctionIR::Hint(hint) => match hint {
751 HintIR::Sorted(sorted_vec) => {
752 let sorted_info: Vec<_> = sorted_vec
753 .iter()
754 .map(|s| (s.column.as_str(), s.descending, s.nulls_last))
755 .collect();
756 ("hint_sorted", sorted_info).into_py_any(py)?
757 },
758 },
759 },
760 }
761 .into_py_any(py),
762 IR::Union { inputs, options } => Union {
763 inputs: inputs.iter().map(|n| n.0).collect(),
764 slice: options.slice,
766 rows: options.rows,
767 maintain_order: options.maintain_order,
768 }
769 .into_py_any(py),
770 IR::HConcat {
771 inputs,
772 schema: _,
773 options,
774 } => HConcat {
775 inputs: inputs.iter().map(|n| n.0).collect(),
776 options: (
777 options.parallel,
778 options.strict,
779 options.broadcast_unit_length,
780 )
781 .into_py_any(py)?,
782 }
783 .into_py_any(py),
784 IR::ExtContext {
785 input,
786 contexts,
787 schema: _,
788 } => ExtContext {
789 input: input.0,
790 contexts: contexts.iter().map(|n| n.0).collect(),
791 }
792 .into_py_any(py),
793 IR::Sink { input, payload } => Sink {
794 input: input.0,
795 payload: PyString::new(
796 py,
797 &serde_json::to_string(payload)
798 .map_err(|err| PyValueError::new_err(format!("{err:?}")))?,
799 )
800 .into(),
801 }
802 .into_py_any(py),
803 IR::SinkMultiple { .. } => Err(PyNotImplementedError::new_err(
804 "Not expecting to see a SinkMultiple node",
805 )),
806 #[cfg(feature = "merge_sorted")]
807 IR::MergeSorted {
808 input_left,
809 input_right,
810 key,
811 maintain_order,
812 } => MergeSorted {
813 input_left: input_left.0,
814 input_right: input_right.0,
815 key: key.iter().map(|k| k.to_string()).collect(),
816 maintain_order: *maintain_order,
817 }
818 .into_py_any(py),
819 IR::UnoptimizedDispatch { .. } => Err(PyNotImplementedError::new_err(
820 "Not expecting to see a UnoptimizedDispatch node",
821 )),
822 IR::Invalid => Err(PyNotImplementedError::new_err("Invalid")),
823 }
824}