1use std::sync::Arc;
17
18use datafusion::arrow::datatypes::{DataType, Schema, SchemaRef};
19use datafusion::catalog::{CatalogProvider, TableProvider};
20use datafusion::common::utils::datafusion_strsim::levenshtein;
21use datafusion::common::Constraint;
22use datafusion::common::{ResolvedTableReference, SchemaReference, TableReference};
23use datafusion::error::{DataFusionError, Result};
24use datafusion::execution::context::SessionState;
25use datafusion::logical_expr::{DdlStatement, LogicalPlan};
26use datafusion::prelude::SessionContext;
27
28use super::schema::{graph_table, lookup_table, GraphSchema, EDGES, ID, NODES};
29use super::{
30 registry, EdgeTable, FileTable, GraphRegistry, Label, Properties, PropertyGraph, VertexTable,
31};
32use crate::file_table::open_file_table;
33use crate::paths::looks_like_location;
34
35const INFORMATION_SCHEMA: &str = "information_schema";
37
38#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct GraphSpec {
42 pub name: String,
43 pub vertex_tables: Vec<VertexSpec>,
44 pub edge_tables: Vec<EdgeSpec>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct VertexSpec {
50 pub name: String,
51 pub table: TableReference,
52 pub key: Option<String>,
53 pub labels: Vec<Label>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct EdgeSpec {
59 pub edge: EdgeTable,
60 pub source_column: Option<String>,
62 pub destination_column: Option<String>,
64}
65
66impl From<super::GraphDef> for GraphSpec {
69 fn from(def: super::GraphDef) -> Self {
70 GraphSpec {
71 name: def.name,
72 vertex_tables: def
73 .vertices
74 .into_iter()
75 .map(|v| VertexSpec {
76 name: v.name,
77 table: v.table,
78 key: Some(v.key),
79 labels: v.labels,
80 })
81 .collect(),
82 edge_tables: vec![EdgeSpec {
83 edge: def.edges,
84 source_column: None,
85 destination_column: None,
86 }],
87 }
88 }
89}
90
91pub async fn create_graph(
95 ctx: &SessionContext,
96 spec: impl Into<GraphSpec>,
97 or_replace: bool,
98 if_not_exists: bool,
99) -> Result<()> {
100 let spec = spec.into();
101 if or_replace && if_not_exists {
102 return plan_err("'IF NOT EXISTS' cannot coexist with 'REPLACE'");
103 }
104 let state = ctx.state();
105 let graphs = registry(&state);
106 if graphs.get(&spec.name).is_some() {
107 if if_not_exists {
108 return Ok(());
109 }
110 if !or_replace {
111 return plan_err(format!("graph '{}' already exists", spec.name));
112 }
113 } else if spec.name == INFORMATION_SCHEMA
114 || default_catalog(&state)?.schema(&spec.name).is_some()
115 {
116 return plan_err(format!(
117 "cannot name a graph '{}': a schema of that name already exists, and a graph's \
118 name is the name of the schema holding its nodes and edges tables",
119 spec.name
120 ));
121 }
122 check_element_names(&spec)?;
123
124 let mut files = Vec::new();
125 let mut vertex_providers = Vec::new();
126 for v in &spec.vertex_tables {
127 vertex_providers.push(open_table(&state, &v.table, &mut files).await?);
128 }
129 let mut edge_schemas = Vec::new();
130 for e in &spec.edge_tables {
131 edge_schemas.push(
132 open_table(&state, &e.edge.table, &mut files)
133 .await?
134 .schema(),
135 );
136 }
137 let def = resolve_keys(&spec, &vertex_providers)?;
138 let mut schemas: Vec<(TableReference, SchemaRef)> = Vec::new();
139 for (v, provider) in def.vertex_tables.iter().zip(&vertex_providers) {
140 schemas.push((v.table.clone(), provider.schema()));
141 }
142 for (e, schema) in def.edge_tables.iter().zip(edge_schemas) {
143 schemas.push((e.table.clone(), schema));
144 }
145 validate(&def, &|t| {
146 schemas
147 .iter()
148 .find(|(table, _)| table == t)
149 .map(|(_, s)| Arc::clone(s))
150 .expect("every table of the graph was opened")
151 })?;
152
153 let name = def.name.clone();
154 let has_tables = def.default_pair().is_some();
155 graphs.put_with_files(def, files);
156 let catalog = default_catalog_inner(&state)?;
157 let schema = match catalog.schema(&name) {
158 Some(schema) => schema,
159 None => {
160 let schema = Arc::new(GraphSchema::new(name.clone(), ctx.state_weak_ref()));
161 catalog.register_schema(&name, Arc::clone(&schema) as _)?;
162 schema
163 }
164 };
165 if has_tables {
168 for table in [NODES, EDGES] {
169 schema.table(table).await?;
170 }
171 }
172 Ok(())
173}
174
175fn check_element_names(spec: &GraphSpec) -> Result<()> {
179 let names: Vec<&str> = spec
180 .vertex_tables
181 .iter()
182 .map(|v| v.name.as_str())
183 .chain(spec.edge_tables.iter().map(|e| e.edge.name.as_str()))
184 .collect();
185 for (i, name) in names.iter().enumerate() {
186 if names[..i].iter().any(|n| n.eq_ignore_ascii_case(name)) {
187 return plan_err(format!(
188 "graph '{}' has two element tables named '{name}'; give one a different name \
189 with AS",
190 spec.name
191 ));
192 }
193 }
194 for e in &spec.edge_tables {
195 for (side, key, vertex, column) in endpoints(e) {
196 let (Some(vertex), Some(column)) = (vertex, column) else {
197 continue;
198 };
199 if spec.vertex_tables.iter().any(|v| &v.name == vertex) {
200 continue;
201 }
202 return plan_err(match spec.vertex_tables.as_slice() {
203 [] => format!(
204 "{side} KEY ({key}) REFERENCES {vertex}, but the graph has no VERTEX \
205 TABLES; add VERTEX TABLES ({vertex} KEY ({column})) or drop the \
206 REFERENCES clause"
207 ),
208 [only] => format!(
209 "{side} KEY ({key}) REFERENCES {vertex} ({column}), but the vertex table is \
210 {} KEY ({}); REFERENCES must name the vertex table and its key",
211 only.table,
212 only.key.as_deref().unwrap_or("…")
213 ),
214 several => {
215 let names: Vec<&str> = several.iter().map(|v| v.name.as_str()).collect();
216 format!(
217 "{side} KEY ({key}) of edge table '{}' REFERENCES {vertex}, which is not \
218 a vertex table of the graph; its vertex tables are {}",
219 e.edge.name,
220 names.join(", ")
221 )
222 }
223 });
224 }
225 if spec.vertex_tables.len() > 1
226 && (e.edge.source_vertex.is_none() || e.edge.destination_vertex.is_none())
227 {
228 let side = if e.edge.source_vertex.is_none() {
229 "SOURCE"
230 } else {
231 "DESTINATION"
232 };
233 return plan_err(format!(
234 "the {side} of edge table '{}' needs REFERENCES: graph '{}' has several vertex \
235 tables, so say which one it refers to",
236 e.edge.name, spec.name
237 ));
238 }
239 }
240 Ok(())
241}
242
243fn resolve_keys(spec: &GraphSpec, providers: &[Arc<dyn TableProvider>]) -> Result<PropertyGraph> {
247 let mut vertex_tables = Vec::new();
248 for (v, provider) in spec.vertex_tables.iter().zip(providers) {
249 let referenced: Vec<(&str, &str, &str)> = spec
252 .edge_tables
253 .iter()
254 .flat_map(endpoints)
255 .filter_map(|(side, key, vertex, column)| match (vertex, column) {
256 (Some(vertex), Some(column)) if vertex == &v.name => {
257 Some((side, key, column.as_str()))
258 }
259 _ => None,
260 })
261 .collect();
262 let key = match &v.key {
263 Some(key) => key.clone(),
264 None => match primary_key(provider.as_ref()) {
265 Some(key) => key,
266 None => match referenced.first() {
267 Some((_, _, column)) => column.to_string(),
268 None => {
269 return plan_err(format!(
270 "vertex table '{}' needs a key: write KEY (column), declare a \
271 primary key, or have an edge table REFERENCES it",
272 v.name
273 ))
274 }
275 },
276 },
277 };
278 if let Some((side, endpoint, column)) = referenced.iter().find(|(_, _, c)| *c != key) {
279 return plan_err(format!(
280 "{side} KEY ({endpoint}) REFERENCES {} ({column}), but the vertex table is {} \
281 KEY ({key}); REFERENCES must name the vertex table and its key",
282 v.name, v.table
283 ));
284 }
285 vertex_tables.push(VertexTable {
286 name: v.name.clone(),
287 table: v.table.clone(),
288 key,
289 labels: v.labels.clone(),
290 });
291 }
292 Ok(PropertyGraph {
293 name: spec.name.clone(),
294 vertex_tables,
295 edge_tables: spec.edge_tables.iter().map(|e| e.edge.clone()).collect(),
296 })
297}
298
299fn endpoints(e: &EdgeSpec) -> [(&'static str, &str, Option<&String>, Option<&String>); 2] {
302 [
303 (
304 "SOURCE",
305 e.edge.source.as_str(),
306 e.edge.source_vertex.as_ref(),
307 e.source_column.as_ref(),
308 ),
309 (
310 "DESTINATION",
311 e.edge.destination.as_str(),
312 e.edge.destination_vertex.as_ref(),
313 e.destination_column.as_ref(),
314 ),
315 ]
316}
317
318fn primary_key(provider: &dyn TableProvider) -> Option<String> {
320 let schema = provider.schema();
321 provider.constraints()?.iter().find_map(|c| match c {
322 Constraint::PrimaryKey(columns) if columns.len() == 1 => {
323 Some(schema.field(columns[0]).name().clone())
324 }
325 _ => None,
326 })
327}
328
329async fn open_table(
333 state: &SessionState,
334 table: &TableReference,
335 files: &mut Vec<FileTable>,
336) -> Result<Arc<dyn TableProvider>> {
337 if let Some(provider) = session_table(state, table).await? {
338 return Ok(provider);
339 }
340 if let TableReference::Bare { table: location } = table {
341 if looks_like_location(location) {
342 let provider = open_file_table(state, location).await?;
343 files.push(FileTable {
344 table: table.clone(),
345 provider: Arc::clone(&provider),
346 });
347 return Ok(provider);
348 }
349 }
350 find_table(state, table).await
351}
352
353async fn session_table(
356 state: &SessionState,
357 table: &TableReference,
358) -> Result<Option<Arc<dyn TableProvider>>> {
359 let Some(catalogs) = registry(state).catalogs().cloned() else {
360 return lookup_table(state, table).await;
361 };
362 let resolved = resolve(state, table);
363 match catalogs
364 .catalog(&resolved.catalog)
365 .and_then(|c| c.schema(&resolved.schema))
366 {
367 Some(schema) => schema.table(&resolved.table).await,
368 None => Ok(None),
369 }
370}
371
372pub fn unknown_graph(name: &str, graphs: &GraphRegistry) -> DataFusionError {
374 let names = graphs.names();
375 let hint = match closest_name(name, names.iter().map(String::as_str)) {
376 Some(s) => format!("; did you mean '{s}'?"),
377 None if names.is_empty() => {
378 "; no graphs are defined (define one with CREATE PROPERTY GRAPH)".to_string()
379 }
380 None => format!(". The graphs are: {}", names.join(", ")),
381 };
382 DataFusionError::Plan(format!("graph '{name}' does not exist{hint}"))
383}
384
385pub async fn check_ddl(state: &SessionState, plan: &LogicalPlan) -> Result<()> {
395 let LogicalPlan::Ddl(ddl) = plan else {
396 return Ok(());
397 };
398 match ddl {
399 DdlStatement::DropTable(drop) => check_drop(state, "table", &drop.name),
400 DdlStatement::DropView(drop) => check_drop(state, "view", &drop.name),
401 DdlStatement::DropCatalogSchema(drop) => check_drop_schema(state, &drop.name),
402 DdlStatement::CreateView(create) if create.or_replace => {
403 let schema = create.input.schema().as_arrow();
404 check_replace(state, "view", &create.name, Some(schema)).await
405 }
406 DdlStatement::CreateMemoryTable(create) if create.or_replace => {
407 let schema = create.input.schema().as_arrow();
408 check_replace(state, "table", &create.name, Some(schema)).await
409 }
410 DdlStatement::CreateExternalTable(create) if create.or_replace => {
411 let schema = create.schema.as_arrow();
414 let known = (!schema.fields().is_empty()).then_some(schema);
415 check_replace(state, "table", &create.name, known).await
416 }
417 _ => Ok(()),
418 }
419}
420
421pub fn check_drop(state: &SessionState, kind: &str, table: &TableReference) -> Result<()> {
423 let dropped = resolve(state, table);
424 let users = graphs_using(state, |t| t == &dropped);
425 if users.is_empty() {
426 return Ok(());
427 }
428 plan_err(format!(
429 "cannot drop {kind} '{table}': {}; drop {} first",
430 used_by(&users),
431 drop_statements(&users)
432 ))
433}
434
435pub fn check_drop_schema(state: &SessionState, schema: &SchemaReference) -> Result<()> {
438 let options = &state.config_options().catalog;
439 let (catalog, name) = match schema {
440 SchemaReference::Bare { schema } => (options.default_catalog.as_str(), schema.as_ref()),
441 SchemaReference::Full { schema, catalog } => (catalog.as_ref(), schema.as_ref()),
442 };
443 if catalog == options.default_catalog && registry(state).get(name).is_some() {
444 return plan_err(format!(
445 "cannot drop schema '{name}': it holds the tables of graph '{name}'; drop the \
446 graph with DROP PROPERTY GRAPH {name}"
447 ));
448 }
449 let users = graphs_using(state, |t| {
450 t.catalog.as_ref() == catalog && t.schema.as_ref() == name
451 });
452 if users.is_empty() {
453 return Ok(());
454 }
455 plan_err(format!(
456 "cannot drop schema '{name}': {} a table in it; drop {} first",
457 if users.len() == 1 {
458 format!("graph '{}' uses", users[0].name)
459 } else {
460 used_by(&users).replace(" use it", " use")
461 },
462 drop_statements(&users)
463 ))
464}
465
466pub async fn check_replace(
471 state: &SessionState,
472 kind: &str,
473 table: &TableReference,
474 schema: Option<&Schema>,
475) -> Result<()> {
476 let replaced = resolve(state, table);
477 let users = graphs_using(state, |t| t == &replaced);
478 if users.is_empty() {
479 return Ok(());
480 }
481 let Some(schema) = schema else {
482 return plan_err(format!(
483 "cannot replace {kind} '{table}' by one whose columns are not known yet: {}; \
484 drop {} first",
485 used_by(&users),
486 drop_statements(&users)
487 ));
488 };
489 let graphs = registry(state);
490 for def in &users {
491 let mut schemas: Vec<(TableReference, SchemaRef)> = Vec::new();
492 for t in def.tables() {
493 let schema =
494 if graphs.file_table(&def.name, t).is_none() && resolve(state, t) == replaced {
495 Arc::new(schema.clone())
496 } else {
497 graph_table(state, &def.name, t).await?.schema()
498 };
499 schemas.push((t.clone(), schema));
500 }
501 let schema_of = |t: &TableReference| {
502 schemas
503 .iter()
504 .find(|(table, _)| table == t)
505 .map(|(_, s)| Arc::clone(s))
506 .expect("every table of the graph was looked up")
507 };
508 if let Err(e) = validate(def, &schema_of) {
509 return plan_err(format!(
510 "cannot replace {kind} '{table}': graph '{}' uses it, and the new {kind} does \
511 not fit its definition ({}). Keep the columns the graph uses, or drop \
512 property graph {} first",
513 def.name,
514 crate::user_message(&e),
515 def.name
516 ));
517 }
518 }
519 Ok(())
520}
521
522fn graphs_using(
524 state: &SessionState,
525 uses: impl Fn(&ResolvedTableReference) -> bool,
526) -> Vec<Arc<PropertyGraph>> {
527 let graphs = registry(state);
528 graphs
529 .all()
530 .into_iter()
531 .filter(|def| {
532 def.tables()
533 .any(|t| graphs.file_table(&def.name, t).is_none() && uses(&resolve(state, t)))
534 })
535 .collect()
536}
537
538fn used_by(graphs: &[Arc<PropertyGraph>]) -> String {
540 let names: Vec<String> = graphs.iter().map(|g| format!("'{}'", g.name)).collect();
541 if names.len() == 1 {
542 format!("graph {} uses it", names[0])
543 } else {
544 format!("graphs {} use it", names.join(", "))
545 }
546}
547
548fn drop_statements(graphs: &[Arc<PropertyGraph>]) -> String {
550 let names: Vec<&str> = graphs.iter().map(|g| g.name.as_str()).collect();
551 if names.len() == 1 {
552 format!("property graph {}", names[0])
553 } else {
554 format!("property graphs {}", names.join(", "))
555 }
556}
557
558fn resolve(state: &SessionState, table: &TableReference) -> ResolvedTableReference {
560 let options = &state.config_options().catalog;
561 table
562 .clone()
563 .resolve(&options.default_catalog, &options.default_schema)
564}
565
566pub fn drop_graph(ctx: &SessionContext, name: &str, if_exists: bool) -> Result<()> {
569 let state = ctx.state();
570 let graphs = registry(&state);
571 if graphs.remove(name).is_none() {
572 if if_exists {
573 return Ok(());
574 }
575 return Err(unknown_graph(name, &graphs));
576 }
577 let catalog = default_catalog_inner(&state)?;
578 if catalog
579 .schema(name)
580 .is_some_and(|schema| schema.is::<GraphSchema>())
581 {
582 catalog.deregister_schema(name, true)?;
583 }
584 Ok(())
585}
586
587pub fn closest_name<'a>(
590 name: &str,
591 candidates: impl IntoIterator<Item = &'a str>,
592) -> Option<&'a str> {
593 let limit = (name.chars().count() / 3).max(2);
594 candidates
595 .into_iter()
596 .map(|c| (levenshtein(name, c), c))
597 .filter(|(d, _)| *d <= limit)
598 .min_by_key(|(d, c)| (*d, *c))
599 .map(|(_, c)| c)
600}
601
602fn did_you_mean<'a>(name: &str, candidates: impl IntoIterator<Item = &'a str>) -> String {
604 closest_name(name, candidates)
605 .map(|c| format!("; did you mean '{c}'?"))
606 .unwrap_or_default()
607}
608
609fn validate(def: &PropertyGraph, schema_of: &dyn Fn(&TableReference) -> SchemaRef) -> Result<()> {
614 for e in &def.edge_tables {
617 let edges = schema_of(&e.table);
618 let table = &e.table;
619 let source = column_type(&edges, table, &e.source, "SOURCE KEY")?;
620 let destination = column_type(&edges, table, &e.destination, "DESTINATION KEY")?;
621 if source != destination {
622 return plan_err(format!(
623 "the source and destination columns of edge table '{table}' must have the same \
624 type, but {} is {source} and {} is {destination}",
625 e.source, e.destination
626 ));
627 }
628 let Some(family) = IdFamily::of(&source) else {
629 return plan_err(format!(
630 "the endpoints of edge table '{table}' ({} and {}) are {source}; node ids must \
631 be integers or strings",
632 e.source, e.destination
633 ));
634 };
635 for v in [def.endpoint_vertex(e, true), def.endpoint_vertex(e, false)]
636 .into_iter()
637 .flatten()
638 {
639 let key = column_type(&schema_of(&v.table), &v.table, &v.key, "KEY")?;
640 match IdFamily::of(&key) {
641 None => return not_an_id_type(v, &key),
642 Some(f) if f != family => {
643 return plan_err(format!(
644 "the key of vertex table '{}' ({}) is {key}, but the endpoints of edge \
645 table '{table}' are {source}; both must be integers or both strings",
646 v.table, v.key
647 ))
648 }
649 Some(_) => {}
650 }
651 }
652 check_properties(&e.labels, &edges, table)?;
653 }
654 for v in &def.vertex_tables {
655 let vertices = schema_of(&v.table);
656 let key = column_type(&vertices, &v.table, &v.key, "KEY")?;
657 if IdFamily::of(&key).is_none() {
658 return not_an_id_type(v, &key);
659 }
660 if v.key != ID && vertices.field_with_name(ID).is_ok() {
661 return plan_err(format!(
662 "vertex table '{}' has a column named '{ID}' that is not its key ({}); \
663 {}.nodes names the key '{ID}', so rename that column (in a view) first",
664 v.table, v.key, def.name
665 ));
666 }
667 check_properties(&v.labels, &vertices, &v.table)?;
668 }
669 Ok(())
670}
671
672fn not_an_id_type<T>(v: &VertexTable, key: &DataType) -> Result<T> {
673 plan_err(format!(
674 "the key of vertex table '{}' ({}) is {key}; node ids must be integers or strings",
675 v.table, v.key
676 ))
677}
678
679fn check_properties(labels: &[Label], schema: &Schema, table: &TableReference) -> Result<()> {
681 for label in labels {
682 let columns: Vec<&String> = match &label.properties {
683 Properties::AllColumns { except } => except.iter().collect(),
684 Properties::Columns(columns) => columns.iter().map(|(c, _)| c).collect(),
685 Properties::None => Vec::new(),
686 };
687 for column in columns {
688 column_type(
689 schema,
690 table,
691 column,
692 &format!("label {} property", label.name),
693 )?;
694 }
695 }
696 Ok(())
697}
698
699#[derive(Debug, PartialEq, Eq, Clone, Copy)]
701enum IdFamily {
702 Integer,
703 String,
704}
705
706impl IdFamily {
707 fn of(data_type: &DataType) -> Option<Self> {
708 match data_type {
709 t if t.is_integer() => Some(Self::Integer),
710 DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => Some(Self::String),
711 DataType::Dictionary(_, value) => match Self::of(value) {
712 Some(Self::String) => Some(Self::String),
713 _ => None,
714 },
715 _ => None,
716 }
717 }
718}
719
720async fn find_table(
722 state: &SessionState,
723 table: &TableReference,
724) -> Result<Arc<dyn TableProvider>> {
725 match lookup_table(state, table).await? {
726 Some(provider) => Ok(provider),
727 None => {
728 let names = table_names(state).await;
729 plan_err(format!(
730 "table '{table}' not found{}",
731 did_you_mean(&table.to_string(), names.iter().map(String::as_str))
732 ))
733 }
734 }
735}
736
737async fn table_names(state: &SessionState) -> Vec<String> {
740 let options = &state.config_options().catalog;
741 let catalogs = state.catalog_list();
742 let mut names = Vec::new();
743 for catalog_name in catalogs.catalog_names() {
744 let Some(catalog) = catalogs.catalog(&catalog_name) else {
745 continue;
746 };
747 for schema_name in catalog.schema_names() {
748 if schema_name == INFORMATION_SCHEMA {
749 continue;
750 }
751 let Some(schema) = catalog.schema(&schema_name) else {
752 continue;
753 };
754 for table in schema.table_names() {
755 let reference = if catalog_name != options.default_catalog {
756 TableReference::full(catalog_name.as_str(), schema_name.as_str(), table)
757 } else if schema_name != options.default_schema {
758 TableReference::partial(schema_name.as_str(), table)
759 } else {
760 TableReference::bare(table)
761 };
762 names.push(reference.to_string());
763 }
764 }
765 }
766 names
767}
768
769fn column_type(
770 schema: &Schema,
771 table: &TableReference,
772 column: &str,
773 clause: &str,
774) -> Result<DataType> {
775 match schema.field_with_name(column) {
776 Ok(field) => Ok(field.data_type().clone()),
777 Err(_) => {
778 let columns: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
779 let hint = match closest_name(column, columns.iter().copied()) {
780 Some(c) => format!("did you mean '{c}'?"),
781 None => format!("its columns are {}", columns.join(", ")),
782 };
783 plan_err(format!(
784 "{clause} column '{column}' not found in table '{table}'; {hint}"
785 ))
786 }
787 }
788}
789
790fn default_catalog(state: &SessionState) -> Result<Arc<dyn CatalogProvider>> {
792 let name = &state.config_options().catalog.default_catalog;
793 state
794 .catalog_list()
795 .catalog(name)
796 .ok_or_else(|| DataFusionError::Plan(format!("the default catalog '{name}' is missing")))
797}
798
799fn default_catalog_inner(state: &SessionState) -> Result<Arc<dyn CatalogProvider>> {
802 let name = &state.config_options().catalog.default_catalog;
803 match registry(state).catalogs() {
804 Some(catalogs) => catalogs.catalog(name).ok_or_else(|| {
805 DataFusionError::Plan(format!("the default catalog '{name}' is missing"))
806 }),
807 None => default_catalog(state),
808 }
809}
810
811fn plan_err<T>(message: impl Into<String>) -> Result<T> {
812 Err(DataFusionError::Plan(message.into()))
813}