Skip to main content

nodedb_sql/resolver/
text_column.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Whether a column can hold the text a full-text search reads.
4
5use nodedb_types::text_search::TextColumnFault;
6
7use super::columns::ResolvedTable;
8use crate::error::{Result, SqlError};
9use crate::types::SqlDataType;
10
11impl ResolvedTable {
12    /// Accept `column` as the field of `function(column, q)`: a declared
13    /// string column, or any name on an open-schema collection.
14    pub fn check_text_column(&self, function: &str, column: &str) -> Result<()> {
15        let fault = match self.info.columns.iter().find(|c| c.name == column) {
16            Some(c) if c.data_type == SqlDataType::String => return Ok(()),
17            Some(c) => TextColumnFault::NotText {
18                data_type: c
19                    .raw_type
20                    .clone()
21                    .unwrap_or_else(|| format!("{:?}", c.data_type)),
22            },
23            None if self.info.open_schema => return Ok(()),
24            None => TextColumnFault::Undeclared,
25        };
26        Err(SqlError::TextColumn {
27            function: function.to_owned(),
28            collection: self.name.clone(),
29            column: column.to_owned(),
30            fault,
31        })
32    }
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use crate::types::{CollectionInfo, ColumnInfo, EngineType};
39
40    fn column(name: &str, data_type: SqlDataType, raw: &str) -> ColumnInfo {
41        ColumnInfo {
42            name: name.into(),
43            data_type,
44            nullable: true,
45            is_primary_key: false,
46            default: None,
47            raw_type: Some(raw.into()),
48            int_width: None,
49            float_width: None,
50        }
51    }
52
53    fn strict() -> ResolvedTable {
54        let info = CollectionInfo {
55            name: "articles".into(),
56            engine: EngineType::DocumentStrict,
57            columns: vec![
58                column("title", SqlDataType::String, "TEXT"),
59                column("views", SqlDataType::Int64, "INT"),
60            ],
61            primary_key: None,
62            has_auto_tier: false,
63            indexes: Vec::new(),
64            bitemporal: false,
65            primary: nodedb_types::PrimaryEngine::Document,
66            vector_primary: None,
67            partition_strategy: nodedb_types::PartitionStrategy::CollectionHomed,
68            open_schema: false,
69        };
70        ResolvedTable {
71            name: "articles".into(),
72            alias: None,
73            info,
74        }
75    }
76
77    #[test]
78    fn a_text_column_is_accepted() {
79        assert_eq!(strict().check_text_column("text_match", "title"), Ok(()));
80    }
81
82    #[test]
83    fn an_int_column_is_not_text() {
84        let err = strict().check_text_column("bm25_score", "views");
85        assert_eq!(
86            err,
87            Err(SqlError::TextColumn {
88                function: "bm25_score".into(),
89                collection: "articles".into(),
90                column: "views".into(),
91                fault: TextColumnFault::NotText {
92                    data_type: "INT".into()
93                },
94            })
95        );
96    }
97
98    #[test]
99    fn an_undeclared_column_names_the_collection() {
100        let Err(SqlError::TextColumn {
101            collection, fault, ..
102        }) = strict().check_text_column("text_match", "ghost")
103        else {
104            panic!("expected a TextColumn error");
105        };
106        assert_eq!(collection, "articles");
107        assert_eq!(fault, TextColumnFault::Undeclared);
108    }
109
110    #[test]
111    fn an_open_schema_collection_accepts_any_name() {
112        let mut table = strict();
113        table.info.open_schema = true;
114        assert_eq!(table.check_text_column("text_match", "ghost"), Ok(()));
115    }
116}