api/v1/
column_def.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16
17use datatypes::schema::{
18    COMMENT_KEY, ColumnDefaultConstraint, ColumnSchema, FULLTEXT_KEY, FulltextAnalyzer,
19    FulltextBackend, FulltextOptions, INVERTED_INDEX_KEY, SKIPPING_INDEX_KEY, SkippingIndexOptions,
20    SkippingIndexType,
21};
22use greptime_proto::v1::{
23    Analyzer, FulltextBackend as PbFulltextBackend, SkippingIndexType as PbSkippingIndexType,
24};
25use snafu::ResultExt;
26
27use crate::error::{self, ConvertColumnDefaultConstraintSnafu, Result};
28use crate::helper::ColumnDataTypeWrapper;
29use crate::v1::{ColumnDef, ColumnOptions, SemanticType};
30
31/// Key used to store fulltext options in gRPC column options.
32const FULLTEXT_GRPC_KEY: &str = "fulltext";
33/// Key used to store inverted index options in gRPC column options.
34const INVERTED_INDEX_GRPC_KEY: &str = "inverted_index";
35/// Key used to store skip index options in gRPC column options.
36const SKIPPING_INDEX_GRPC_KEY: &str = "skipping_index";
37
38/// Tries to construct a `ColumnSchema` from the given  `ColumnDef`.
39pub fn try_as_column_schema(column_def: &ColumnDef) -> Result<ColumnSchema> {
40    let data_type = ColumnDataTypeWrapper::try_new(
41        column_def.data_type,
42        column_def.datatype_extension.clone(),
43    )?;
44
45    let constraint = if column_def.default_constraint.is_empty() {
46        None
47    } else {
48        Some(
49            ColumnDefaultConstraint::try_from(column_def.default_constraint.as_slice()).context(
50                error::ConvertColumnDefaultConstraintSnafu {
51                    column: &column_def.name,
52                },
53            )?,
54        )
55    };
56
57    let mut metadata = HashMap::new();
58    if !column_def.comment.is_empty() {
59        metadata.insert(COMMENT_KEY.to_string(), column_def.comment.clone());
60    }
61    if let Some(options) = column_def.options.as_ref() {
62        if let Some(fulltext) = options.options.get(FULLTEXT_GRPC_KEY) {
63            metadata.insert(FULLTEXT_KEY.to_string(), fulltext.to_owned());
64        }
65        if let Some(inverted_index) = options.options.get(INVERTED_INDEX_GRPC_KEY) {
66            metadata.insert(INVERTED_INDEX_KEY.to_string(), inverted_index.to_owned());
67        }
68        if let Some(skipping_index) = options.options.get(SKIPPING_INDEX_GRPC_KEY) {
69            metadata.insert(SKIPPING_INDEX_KEY.to_string(), skipping_index.to_owned());
70        }
71    }
72
73    ColumnSchema::new(&column_def.name, data_type.into(), column_def.is_nullable)
74        .with_metadata(metadata)
75        .with_time_index(column_def.semantic_type() == SemanticType::Timestamp)
76        .with_default_constraint(constraint)
77        .context(error::InvalidColumnDefaultConstraintSnafu {
78            column: &column_def.name,
79        })
80}
81
82/// Tries to construct a `ColumnDef` from the given `ColumnSchema`.
83///
84/// TODO(weny): Add tests for this function.
85pub fn try_as_column_def(column_schema: &ColumnSchema, is_primary_key: bool) -> Result<ColumnDef> {
86    let column_datatype =
87        ColumnDataTypeWrapper::try_from(column_schema.data_type.clone()).map(|w| w.to_parts())?;
88
89    let semantic_type = if column_schema.is_time_index() {
90        SemanticType::Timestamp
91    } else if is_primary_key {
92        SemanticType::Tag
93    } else {
94        SemanticType::Field
95    } as i32;
96    let comment = column_schema
97        .metadata()
98        .get(COMMENT_KEY)
99        .cloned()
100        .unwrap_or_default();
101
102    let default_constraint = match column_schema.default_constraint() {
103        None => vec![],
104        Some(v) => v
105            .clone()
106            .try_into()
107            .context(ConvertColumnDefaultConstraintSnafu {
108                column: &column_schema.name,
109            })?,
110    };
111    let options = options_from_column_schema(column_schema);
112    Ok(ColumnDef {
113        name: column_schema.name.clone(),
114        data_type: column_datatype.0 as i32,
115        is_nullable: column_schema.is_nullable(),
116        default_constraint,
117        semantic_type,
118        comment,
119        datatype_extension: column_datatype.1,
120        options,
121    })
122}
123
124/// Constructs a `ColumnOptions` from the given `ColumnSchema`.
125pub fn options_from_column_schema(column_schema: &ColumnSchema) -> Option<ColumnOptions> {
126    let mut options = ColumnOptions::default();
127    if let Some(fulltext) = column_schema.metadata().get(FULLTEXT_KEY) {
128        options
129            .options
130            .insert(FULLTEXT_GRPC_KEY.to_string(), fulltext.to_owned());
131    }
132    if let Some(inverted_index) = column_schema.metadata().get(INVERTED_INDEX_KEY) {
133        options
134            .options
135            .insert(INVERTED_INDEX_GRPC_KEY.to_string(), inverted_index.clone());
136    }
137    if let Some(skipping_index) = column_schema.metadata().get(SKIPPING_INDEX_KEY) {
138        options
139            .options
140            .insert(SKIPPING_INDEX_GRPC_KEY.to_string(), skipping_index.clone());
141    }
142
143    (!options.options.is_empty()).then_some(options)
144}
145
146/// Checks if the `ColumnOptions` contains fulltext options.
147pub fn contains_fulltext(options: &Option<ColumnOptions>) -> bool {
148    options
149        .as_ref()
150        .is_some_and(|o| o.options.contains_key(FULLTEXT_GRPC_KEY))
151}
152
153/// Checks if the `ColumnOptions` contains skipping index options.
154pub fn contains_skipping(options: &Option<ColumnOptions>) -> bool {
155    options
156        .as_ref()
157        .is_some_and(|o| o.options.contains_key(SKIPPING_INDEX_GRPC_KEY))
158}
159
160/// Tries to construct a `ColumnOptions` from the given `FulltextOptions`.
161pub fn options_from_fulltext(fulltext: &FulltextOptions) -> Result<Option<ColumnOptions>> {
162    let mut options = ColumnOptions::default();
163
164    let v = serde_json::to_string(fulltext).context(error::SerializeJsonSnafu)?;
165    options.options.insert(FULLTEXT_GRPC_KEY.to_string(), v);
166
167    Ok((!options.options.is_empty()).then_some(options))
168}
169
170/// Tries to construct a `ColumnOptions` from the given `SkippingIndexOptions`.
171pub fn options_from_skipping(skipping: &SkippingIndexOptions) -> Result<Option<ColumnOptions>> {
172    let mut options = ColumnOptions::default();
173
174    let v = serde_json::to_string(skipping).context(error::SerializeJsonSnafu)?;
175    options
176        .options
177        .insert(SKIPPING_INDEX_GRPC_KEY.to_string(), v);
178
179    Ok((!options.options.is_empty()).then_some(options))
180}
181
182/// Tries to construct a `ColumnOptions` for inverted index.
183pub fn options_from_inverted() -> ColumnOptions {
184    let mut options = ColumnOptions::default();
185    options
186        .options
187        .insert(INVERTED_INDEX_GRPC_KEY.to_string(), "true".to_string());
188    options
189}
190
191/// Tries to construct a `FulltextAnalyzer` from the given analyzer.
192pub fn as_fulltext_option_analyzer(analyzer: Analyzer) -> FulltextAnalyzer {
193    match analyzer {
194        Analyzer::English => FulltextAnalyzer::English,
195        Analyzer::Chinese => FulltextAnalyzer::Chinese,
196    }
197}
198
199/// Tries to construct a `FulltextBackend` from the given backend.
200pub fn as_fulltext_option_backend(backend: PbFulltextBackend) -> FulltextBackend {
201    match backend {
202        PbFulltextBackend::Bloom => FulltextBackend::Bloom,
203        PbFulltextBackend::Tantivy => FulltextBackend::Tantivy,
204    }
205}
206
207/// Tries to construct a `SkippingIndexType` from the given skipping index type.
208pub fn as_skipping_index_type(skipping_index_type: PbSkippingIndexType) -> SkippingIndexType {
209    match skipping_index_type {
210        PbSkippingIndexType::BloomFilter => SkippingIndexType::BloomFilter,
211    }
212}
213
214#[cfg(test)]
215mod tests {
216
217    use datatypes::data_type::ConcreteDataType;
218    use datatypes::schema::{FulltextAnalyzer, FulltextBackend};
219
220    use super::*;
221    use crate::v1::ColumnDataType;
222
223    #[test]
224    fn test_try_as_column_schema() {
225        let column_def = ColumnDef {
226            name: "test".to_string(),
227            data_type: ColumnDataType::String as i32,
228            is_nullable: true,
229            default_constraint: ColumnDefaultConstraint::Value("test_default".into())
230                .try_into()
231                .unwrap(),
232            semantic_type: SemanticType::Field as i32,
233            comment: "test_comment".to_string(),
234            datatype_extension: None,
235            options: Some(ColumnOptions {
236                options: HashMap::from([
237                    (
238                        FULLTEXT_GRPC_KEY.to_string(),
239                        "{\"enable\":true}".to_string(),
240                    ),
241                    (INVERTED_INDEX_GRPC_KEY.to_string(), "true".to_string()),
242                ]),
243            }),
244        };
245
246        let schema = try_as_column_schema(&column_def).unwrap();
247        assert_eq!(schema.name, "test");
248        assert_eq!(schema.data_type, ConcreteDataType::string_datatype());
249        assert!(!schema.is_time_index());
250        assert!(schema.is_nullable());
251        assert_eq!(
252            schema.default_constraint().unwrap(),
253            &ColumnDefaultConstraint::Value("test_default".into())
254        );
255        assert_eq!(schema.metadata().get(COMMENT_KEY).unwrap(), "test_comment");
256        assert_eq!(
257            schema.fulltext_options().unwrap().unwrap(),
258            FulltextOptions {
259                enable: true,
260                ..Default::default()
261            }
262        );
263        assert!(schema.is_inverted_indexed());
264    }
265
266    #[test]
267    fn test_options_from_column_schema() {
268        let schema = ColumnSchema::new("test", ConcreteDataType::string_datatype(), true);
269        let options = options_from_column_schema(&schema);
270        assert!(options.is_none());
271
272        let mut schema = ColumnSchema::new("test", ConcreteDataType::string_datatype(), true)
273            .with_fulltext_options(FulltextOptions::new_unchecked(
274                true,
275                FulltextAnalyzer::English,
276                false,
277                FulltextBackend::Bloom,
278                10240,
279                0.01,
280            ))
281            .unwrap();
282        schema.set_inverted_index(true);
283        let options = options_from_column_schema(&schema).unwrap();
284        assert_eq!(
285            options.options.get(FULLTEXT_GRPC_KEY).unwrap(),
286            "{\"enable\":true,\"analyzer\":\"English\",\"case-sensitive\":false,\"backend\":\"bloom\",\"granularity\":10240,\"false-positive-rate-in-10000\":100}"
287        );
288        assert_eq!(
289            options.options.get(INVERTED_INDEX_GRPC_KEY).unwrap(),
290            "true"
291        );
292    }
293
294    #[test]
295    fn test_options_with_fulltext() {
296        let fulltext = FulltextOptions::new_unchecked(
297            true,
298            FulltextAnalyzer::English,
299            false,
300            FulltextBackend::Bloom,
301            10240,
302            0.01,
303        );
304        let options = options_from_fulltext(&fulltext).unwrap().unwrap();
305        assert_eq!(
306            options.options.get(FULLTEXT_GRPC_KEY).unwrap(),
307            "{\"enable\":true,\"analyzer\":\"English\",\"case-sensitive\":false,\"backend\":\"bloom\",\"granularity\":10240,\"false-positive-rate-in-10000\":100}"
308        );
309    }
310
311    #[test]
312    fn test_contains_fulltext() {
313        let options = ColumnOptions {
314            options: HashMap::from([(
315                FULLTEXT_GRPC_KEY.to_string(),
316                "{\"enable\":true}".to_string(),
317            )]),
318        };
319        assert!(contains_fulltext(&Some(options)));
320
321        let options = ColumnOptions {
322            options: HashMap::new(),
323        };
324        assert!(!contains_fulltext(&Some(options)));
325
326        assert!(!contains_fulltext(&None));
327    }
328}