1use std::collections::HashMap;
16
17use datatypes::schema::{
18 ColumnDefaultConstraint, ColumnSchema, FulltextAnalyzer, FulltextBackend, FulltextOptions,
19 SkippingIndexOptions, SkippingIndexType, COMMENT_KEY, FULLTEXT_KEY, INVERTED_INDEX_KEY,
20 SKIPPING_INDEX_KEY,
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
31const FULLTEXT_GRPC_KEY: &str = "fulltext";
33const INVERTED_INDEX_GRPC_KEY: &str = "inverted_index";
35const SKIPPING_INDEX_GRPC_KEY: &str = "skipping_index";
37
38pub fn try_as_column_schema(column_def: &ColumnDef) -> Result<ColumnSchema> {
40 let data_type =
41 ColumnDataTypeWrapper::try_new(column_def.data_type, column_def.datatype_extension)?;
42
43 let constraint = if column_def.default_constraint.is_empty() {
44 None
45 } else {
46 Some(
47 ColumnDefaultConstraint::try_from(column_def.default_constraint.as_slice()).context(
48 error::ConvertColumnDefaultConstraintSnafu {
49 column: &column_def.name,
50 },
51 )?,
52 )
53 };
54
55 let mut metadata = HashMap::new();
56 if !column_def.comment.is_empty() {
57 metadata.insert(COMMENT_KEY.to_string(), column_def.comment.clone());
58 }
59 if let Some(options) = column_def.options.as_ref() {
60 if let Some(fulltext) = options.options.get(FULLTEXT_GRPC_KEY) {
61 metadata.insert(FULLTEXT_KEY.to_string(), fulltext.to_owned());
62 }
63 if let Some(inverted_index) = options.options.get(INVERTED_INDEX_GRPC_KEY) {
64 metadata.insert(INVERTED_INDEX_KEY.to_string(), inverted_index.to_owned());
65 }
66 if let Some(skipping_index) = options.options.get(SKIPPING_INDEX_GRPC_KEY) {
67 metadata.insert(SKIPPING_INDEX_KEY.to_string(), skipping_index.to_owned());
68 }
69 }
70
71 ColumnSchema::new(&column_def.name, data_type.into(), column_def.is_nullable)
72 .with_metadata(metadata)
73 .with_time_index(column_def.semantic_type() == SemanticType::Timestamp)
74 .with_default_constraint(constraint)
75 .context(error::InvalidColumnDefaultConstraintSnafu {
76 column: &column_def.name,
77 })
78}
79
80pub fn try_as_column_def(column_schema: &ColumnSchema, is_primary_key: bool) -> Result<ColumnDef> {
84 let column_datatype =
85 ColumnDataTypeWrapper::try_from(column_schema.data_type.clone()).map(|w| w.to_parts())?;
86
87 let semantic_type = if column_schema.is_time_index() {
88 SemanticType::Timestamp
89 } else if is_primary_key {
90 SemanticType::Tag
91 } else {
92 SemanticType::Field
93 } as i32;
94 let comment = column_schema
95 .metadata()
96 .get(COMMENT_KEY)
97 .cloned()
98 .unwrap_or_default();
99
100 let default_constraint = match column_schema.default_constraint() {
101 None => vec![],
102 Some(v) => v
103 .clone()
104 .try_into()
105 .context(ConvertColumnDefaultConstraintSnafu {
106 column: &column_schema.name,
107 })?,
108 };
109 let options = options_from_column_schema(column_schema);
110 Ok(ColumnDef {
111 name: column_schema.name.clone(),
112 data_type: column_datatype.0 as i32,
113 is_nullable: column_schema.is_nullable(),
114 default_constraint,
115 semantic_type,
116 comment,
117 datatype_extension: column_datatype.1,
118 options,
119 })
120}
121
122pub fn options_from_column_schema(column_schema: &ColumnSchema) -> Option<ColumnOptions> {
124 let mut options = ColumnOptions::default();
125 if let Some(fulltext) = column_schema.metadata().get(FULLTEXT_KEY) {
126 options
127 .options
128 .insert(FULLTEXT_GRPC_KEY.to_string(), fulltext.to_owned());
129 }
130 if let Some(inverted_index) = column_schema.metadata().get(INVERTED_INDEX_KEY) {
131 options
132 .options
133 .insert(INVERTED_INDEX_GRPC_KEY.to_string(), inverted_index.clone());
134 }
135 if let Some(skipping_index) = column_schema.metadata().get(SKIPPING_INDEX_KEY) {
136 options
137 .options
138 .insert(SKIPPING_INDEX_GRPC_KEY.to_string(), skipping_index.clone());
139 }
140
141 (!options.options.is_empty()).then_some(options)
142}
143
144pub fn contains_fulltext(options: &Option<ColumnOptions>) -> bool {
146 options
147 .as_ref()
148 .is_some_and(|o| o.options.contains_key(FULLTEXT_GRPC_KEY))
149}
150
151pub fn contains_skipping(options: &Option<ColumnOptions>) -> bool {
153 options
154 .as_ref()
155 .is_some_and(|o| o.options.contains_key(SKIPPING_INDEX_GRPC_KEY))
156}
157
158pub fn options_from_fulltext(fulltext: &FulltextOptions) -> Result<Option<ColumnOptions>> {
160 let mut options = ColumnOptions::default();
161
162 let v = serde_json::to_string(fulltext).context(error::SerializeJsonSnafu)?;
163 options.options.insert(FULLTEXT_GRPC_KEY.to_string(), v);
164
165 Ok((!options.options.is_empty()).then_some(options))
166}
167
168pub fn options_from_skipping(skipping: &SkippingIndexOptions) -> Result<Option<ColumnOptions>> {
170 let mut options = ColumnOptions::default();
171
172 let v = serde_json::to_string(skipping).context(error::SerializeJsonSnafu)?;
173 options
174 .options
175 .insert(SKIPPING_INDEX_GRPC_KEY.to_string(), v);
176
177 Ok((!options.options.is_empty()).then_some(options))
178}
179
180pub fn options_from_inverted() -> ColumnOptions {
182 let mut options = ColumnOptions::default();
183 options
184 .options
185 .insert(INVERTED_INDEX_GRPC_KEY.to_string(), "true".to_string());
186 options
187}
188
189pub fn as_fulltext_option_analyzer(analyzer: Analyzer) -> FulltextAnalyzer {
191 match analyzer {
192 Analyzer::English => FulltextAnalyzer::English,
193 Analyzer::Chinese => FulltextAnalyzer::Chinese,
194 }
195}
196
197pub fn as_fulltext_option_backend(backend: PbFulltextBackend) -> FulltextBackend {
199 match backend {
200 PbFulltextBackend::Bloom => FulltextBackend::Bloom,
201 PbFulltextBackend::Tantivy => FulltextBackend::Tantivy,
202 }
203}
204
205pub fn as_skipping_index_type(skipping_index_type: PbSkippingIndexType) -> SkippingIndexType {
207 match skipping_index_type {
208 PbSkippingIndexType::BloomFilter => SkippingIndexType::BloomFilter,
209 }
210}
211
212#[cfg(test)]
213mod tests {
214
215 use datatypes::data_type::ConcreteDataType;
216 use datatypes::schema::{FulltextAnalyzer, FulltextBackend};
217
218 use super::*;
219 use crate::v1::ColumnDataType;
220
221 #[test]
222 fn test_try_as_column_schema() {
223 let column_def = ColumnDef {
224 name: "test".to_string(),
225 data_type: ColumnDataType::String as i32,
226 is_nullable: true,
227 default_constraint: ColumnDefaultConstraint::Value("test_default".into())
228 .try_into()
229 .unwrap(),
230 semantic_type: SemanticType::Field as i32,
231 comment: "test_comment".to_string(),
232 datatype_extension: None,
233 options: Some(ColumnOptions {
234 options: HashMap::from([
235 (
236 FULLTEXT_GRPC_KEY.to_string(),
237 "{\"enable\":true}".to_string(),
238 ),
239 (INVERTED_INDEX_GRPC_KEY.to_string(), "true".to_string()),
240 ]),
241 }),
242 };
243
244 let schema = try_as_column_schema(&column_def).unwrap();
245 assert_eq!(schema.name, "test");
246 assert_eq!(schema.data_type, ConcreteDataType::string_datatype());
247 assert!(!schema.is_time_index());
248 assert!(schema.is_nullable());
249 assert_eq!(
250 schema.default_constraint().unwrap(),
251 &ColumnDefaultConstraint::Value("test_default".into())
252 );
253 assert_eq!(schema.metadata().get(COMMENT_KEY).unwrap(), "test_comment");
254 assert_eq!(
255 schema.fulltext_options().unwrap().unwrap(),
256 FulltextOptions {
257 enable: true,
258 ..Default::default()
259 }
260 );
261 assert!(schema.is_inverted_indexed());
262 }
263
264 #[test]
265 fn test_options_from_column_schema() {
266 let schema = ColumnSchema::new("test", ConcreteDataType::string_datatype(), true);
267 let options = options_from_column_schema(&schema);
268 assert!(options.is_none());
269
270 let mut schema = ColumnSchema::new("test", ConcreteDataType::string_datatype(), true)
271 .with_fulltext_options(FulltextOptions::new_unchecked(
272 true,
273 FulltextAnalyzer::English,
274 false,
275 FulltextBackend::Bloom,
276 10240,
277 0.01,
278 ))
279 .unwrap();
280 schema.set_inverted_index(true);
281 let options = options_from_column_schema(&schema).unwrap();
282 assert_eq!(
283 options.options.get(FULLTEXT_GRPC_KEY).unwrap(),
284 "{\"enable\":true,\"analyzer\":\"English\",\"case-sensitive\":false,\"backend\":\"bloom\",\"granularity\":10240,\"false-positive-rate-in-10000\":100}"
285 );
286 assert_eq!(
287 options.options.get(INVERTED_INDEX_GRPC_KEY).unwrap(),
288 "true"
289 );
290 }
291
292 #[test]
293 fn test_options_with_fulltext() {
294 let fulltext = FulltextOptions::new_unchecked(
295 true,
296 FulltextAnalyzer::English,
297 false,
298 FulltextBackend::Bloom,
299 10240,
300 0.01,
301 );
302 let options = options_from_fulltext(&fulltext).unwrap().unwrap();
303 assert_eq!(
304 options.options.get(FULLTEXT_GRPC_KEY).unwrap(),
305 "{\"enable\":true,\"analyzer\":\"English\",\"case-sensitive\":false,\"backend\":\"bloom\",\"granularity\":10240,\"false-positive-rate-in-10000\":100}"
306 );
307 }
308
309 #[test]
310 fn test_contains_fulltext() {
311 let options = ColumnOptions {
312 options: HashMap::from([(
313 FULLTEXT_GRPC_KEY.to_string(),
314 "{\"enable\":true}".to_string(),
315 )]),
316 };
317 assert!(contains_fulltext(&Some(options)));
318
319 let options = ColumnOptions {
320 options: HashMap::new(),
321 };
322 assert!(!contains_fulltext(&Some(options)));
323
324 assert!(!contains_fulltext(&None));
325 }
326}