Skip to main content

ursa_engine/graph/
define.rs

1//! Defining and dropping graphs: what `CREATE PROPERTY GRAPH` and
2//! `DROP PROPERTY GRAPH` do once parsed.
3//!
4//! A definition is checked when it is created: its tables exist, element
5//! names are unique, every vertex table has a key (written, or inferred from a
6//! primary key or the edges' `REFERENCES`, as DuckPGQ does), the named columns
7//! and properties exist, each edge table's endpoints have one node id type (an
8//! integer of any width, or a string), and each vertex key's type is
9//! compatible with the endpoints that refer to it. A table given as a file
10//! path is opened then (see the [module docs](super)).
11//!
12//! The tables are read afresh by every query. [`check_ddl`] keeps them from
13//! being dropped, or replaced by ones the definition no longer fits, by SQL
14//! while a graph uses them.
15
16use 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
35/// The schema DataFusion reserves for `information_schema` views.
36const INFORMATION_SCHEMA: &str = "information_schema";
37
38/// A graph as `CREATE PROPERTY GRAPH` wrote it, its identifiers normalized,
39/// before [`create_graph`] resolves its vertex keys.
40#[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/// A vertex table as written: its `KEY` may be left out.
48#[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/// An edge table as written, with the key column each `REFERENCES` names.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct EdgeSpec {
59    pub edge: EdgeTable,
60    /// `SOURCE KEY (…) REFERENCES vertex (column)`: the column.
61    pub source_column: Option<String>,
62    /// `DESTINATION KEY (…) REFERENCES vertex (column)`: the column.
63    pub destination_column: Option<String>,
64}
65
66/// A graph of one edge table and at most one vertex table, as a spec: what
67/// every graph was before #295, and what tests build.
68impl 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
91/// Define `spec` in `ctx`, as `CREATE [OR REPLACE] PROPERTY GRAPH [IF NOT EXISTS]`
92/// does. Follows DataFusion's `CREATE TABLE`: an existing graph is an error,
93/// unless `or_replace` (replace it) or `if_not_exists` (keep it, do nothing).
94pub 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    // Plan both tables once, so their columns are known to listings even if a
166    // table under the graph later disappears.
167    if has_tables {
168        for table in [NODES, EDGES] {
169            schema.table(table).await?;
170        }
171    }
172    Ok(())
173}
174
175/// Element names (an `AS` name, or else the table's name) are unique across
176/// a graph's vertex and edge tables, as SQL/PGQ requires; `REFERENCES` names
177/// a vertex table by its element name.
178fn 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
243/// The graph with every vertex key resolved: `KEY (column)` as written, else
244/// the table's primary key, else the column the edges' `REFERENCES` name (as
245/// DuckPGQ infers it). A `REFERENCES` column must be the key.
246fn 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        // (side, endpoint column, referenced column) of each endpoint that
250        // REFERENCES this vertex table.
251        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
299/// An edge table's two endpoints: (side, column, referenced vertex, referenced
300/// column).
301fn 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
318/// The table's declared single-column primary key, if it has one.
319fn 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
329/// The table `table` names: a table or view in the session, or else, when it
330/// looks like a file path or URL, that file opened as a listing table (added
331/// to `files`).
332async 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
353/// The table or view `table` names in the session's catalogs, not opening
354/// files.
355async 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
372/// The error for a graph that does not exist, with a suggestion.
373pub 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
385/// Refuse a DDL statement, planned as `plan`, that would break a graph:
386///
387/// - `DROP TABLE` or `DROP VIEW` of a table a graph uses;
388/// - `DROP SCHEMA` of a graph's schema, or of a schema holding a table a graph
389///   uses;
390/// - `CREATE OR REPLACE` of a table a graph uses, by one whose columns the
391///   graph's definition no longer fits (see [`check_replace`]).
392///
393/// Every other plan passes.
394pub 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            // An empty schema is inferred from the files when the statement
412            // runs, so it cannot be checked here.
413            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
421/// Refuse to drop `table` (a `kind`: "table" or "view") while a graph uses it.
422pub 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
435/// Refuse to drop the schema `schema` while it is a graph's, or holds a table
436/// a graph uses.
437pub 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
466/// Refuse to replace `table` (a `kind`) by one with the columns `schema`
467/// while a graph uses it and its definition does not fit those columns.
468/// `None`: the new columns are not known yet, so the replacement is refused
469/// whenever a graph uses the table.
470pub 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
522/// The graphs with a session table (not a file path) for which `uses` holds.
523fn 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
538/// "graph 'g' uses it" or "graphs 'g', 'h' use it".
539fn 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
548/// "property graph g" or "property graphs g, h".
549fn 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
558/// `table` with the session's default catalog and schema filled in.
559fn 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
566/// Drop the graph called `name` and its schema, as `DROP PROPERTY GRAPH
567/// [IF EXISTS]` does.
568pub 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
587/// The closest of `candidates` to `name` by edit distance, if any is close
588/// enough to be a likely typo: for "did you mean" suggestions.
589pub 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
602/// "; did you mean 'x'?" for the closest candidate, or nothing.
603fn 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
609/// Check `def` against its tables' columns (`schema_of`): every named column
610/// and property exists, each edge table's endpoints have one node id type,
611/// and each vertex key is a node id type compatible with the endpoints that
612/// refer to it.
613fn validate(def: &PropertyGraph, schema_of: &dyn Fn(&TableReference) -> SchemaRef) -> Result<()> {
614    // Edge tables first, then what is left of the vertex tables: the order a
615    // one-pair graph's checks have always run in, so its errors are unchanged.
616    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
679/// Every column a label's properties name exists in the table.
680fn 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/// Node ids are integers of any width, or strings.
700#[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
720/// The table `table` names, or an error with a suggestion.
721async 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
737/// Every table in the session, named as a query in the default schema would
738/// name it.
739async 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
790/// The default catalog, as queries see it.
791fn 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
799/// The default catalog without the file-table wrapper (which cannot
800/// deregister a schema), when the session was built by `new_session`.
801fn 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}