1use 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
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 = 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
82pub 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
124pub 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
146pub 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
153pub 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
160pub 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
170pub 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
182pub 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
191pub fn as_fulltext_option_analyzer(analyzer: Analyzer) -> FulltextAnalyzer {
193 match analyzer {
194 Analyzer::English => FulltextAnalyzer::English,
195 Analyzer::Chinese => FulltextAnalyzer::Chinese,
196 }
197}
198
199pub fn as_fulltext_option_backend(backend: PbFulltextBackend) -> FulltextBackend {
201 match backend {
202 PbFulltextBackend::Bloom => FulltextBackend::Bloom,
203 PbFulltextBackend::Tantivy => FulltextBackend::Tantivy,
204 }
205}
206
207pub 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}