1use std::collections::HashMap;
16
17use api::v1::column_def::try_as_column_def;
18use api::v1::meta::Partition;
19use api::v1::region::{CreateRequest, RegionColumnDef};
20use api::v1::{ColumnDataType, ColumnDef, CreateTableExpr, SemanticType};
21use common_catalog::consts::{MITO_ENGINE, MITO2_ENGINE};
22use common_telemetry::warn;
23use snafu::{OptionExt, ResultExt};
24use store_api::metric_engine_consts::{
25 DATA_SCHEMA_TABLE_ID_COLUMN_NAME, DATA_SCHEMA_TSID_COLUMN_NAME, LOGICAL_TABLE_METADATA_KEY,
26 PRIMARY_KEY_ENCODING,
27};
28use store_api::region_request::RegionRequirements;
29use store_api::storage::consts::ReservedColumnId;
30use store_api::storage::{RegionId, RegionNumber};
31use table::metadata::{TableId, TableInfo};
32
33use crate::error::{self, Result, SerializeWalOptionsSnafu};
34use crate::reconciliation::utils::build_column_metadata_from_table_info;
35use crate::wal_provider::{RegionWalOptions, serialize_wal_options};
36
37pub fn build_template_from_raw_table_info(table_info: &TableInfo) -> Result<CreateRequest> {
45 let primary_key_indices = &table_info.meta.primary_key_indices;
46 let column_defs = table_info
47 .meta
48 .schema
49 .column_schemas()
50 .iter()
51 .enumerate()
52 .map(|(i, c)| {
53 let is_primary_key = primary_key_indices.contains(&i);
54 let column_def = try_as_column_def(c, is_primary_key)
55 .context(error::ConvertColumnDefSnafu { column: &c.name })?;
56 Ok(RegionColumnDef {
57 column_def: Some(column_def),
58 column_id: i as u32,
61 })
62 })
63 .collect::<Result<Vec<_>>>()?;
64
65 let options = HashMap::from(&table_info.meta.options);
66 let template = CreateRequest {
67 region_id: 0,
68 engine: table_info.meta.engine.clone(),
69 column_defs,
70 primary_key: table_info
71 .meta
72 .primary_key_indices
73 .iter()
74 .map(|i| *i as u32)
75 .collect(),
76 path: String::new(),
77 options,
78 partition: None,
79 requirements: None,
80 };
81
82 Ok(template)
83}
84
85pub fn build_template_from_raw_table_info_for_physical_table(
92 table_info: &TableInfo,
93) -> Result<CreateRequest> {
94 let name_to_ids = table_info
95 .name_to_ids()
96 .context(error::MissingColumnIdsSnafu)?;
97 let column_metadatas = build_column_metadata_from_table_info(
98 table_info.meta.schema.column_schemas(),
99 &table_info.meta.primary_key_indices,
100 &name_to_ids,
101 )?;
102 let column_defs = column_metadatas
103 .iter()
104 .map(|c| {
105 let column_def =
106 try_as_column_def(&c.column_schema, c.semantic_type == SemanticType::Tag).context(
107 error::ConvertColumnDefSnafu {
108 column: &c.column_schema.name,
109 },
110 )?;
111 let region_column_def = RegionColumnDef {
112 column_def: Some(column_def),
113 column_id: c.column_id,
114 };
115
116 Ok(region_column_def)
117 })
118 .collect::<Result<Vec<_>>>()?;
119 let primary_key = table_info
121 .meta
122 .primary_key_indices
123 .iter()
124 .map(|idx| column_metadatas[*idx].column_id)
125 .collect();
126
127 let options = HashMap::from(&table_info.meta.options);
128 let template = CreateRequest {
129 region_id: 0,
130 engine: table_info.meta.engine.clone(),
131 column_defs,
132 primary_key,
133 path: String::new(),
134 options,
135 partition: None,
136 requirements: None,
137 };
138
139 Ok(template)
140}
141
142pub(crate) fn build_template(create_table_expr: &CreateTableExpr) -> Result<CreateRequest> {
143 let reserved_prefix = matches!(
145 create_table_expr.engine.as_str(),
146 MITO_ENGINE | MITO2_ENGINE
147 ) && create_table_expr
148 .table_options
149 .get(PRIMARY_KEY_ENCODING)
150 .is_some_and(|value| value.eq_ignore_ascii_case("sparse"))
151 && create_table_expr.primary_keys.starts_with(&[
152 DATA_SCHEMA_TABLE_ID_COLUMN_NAME.into(),
153 DATA_SCHEMA_TSID_COLUMN_NAME.into(),
154 ])
155 && [
156 (DATA_SCHEMA_TABLE_ID_COLUMN_NAME, ColumnDataType::Uint32),
157 (DATA_SCHEMA_TSID_COLUMN_NAME, ColumnDataType::Uint64),
158 ]
159 .into_iter()
160 .all(|(name, data_type)| {
161 create_table_expr.column_defs.iter().any(|column| {
162 column.name == name && column.data_type == data_type as i32 && !column.is_nullable
163 })
164 });
165 let column_defs = create_table_expr
166 .column_defs
167 .iter()
168 .enumerate()
169 .map(|(i, c)| {
170 let semantic_type = if create_table_expr.time_index == c.name {
171 SemanticType::Timestamp
172 } else if create_table_expr.primary_keys.contains(&c.name) {
173 SemanticType::Tag
174 } else {
175 SemanticType::Field
176 };
177
178 RegionColumnDef {
179 column_def: Some(ColumnDef {
180 name: c.name.clone(),
181 data_type: c.data_type,
182 is_nullable: c.is_nullable,
183 default_constraint: c.default_constraint.clone(),
184 semantic_type: semantic_type as i32,
185 comment: String::new(),
186 datatype_extension: c.datatype_extension.clone(),
187 options: c.options.clone(),
188 }),
189 column_id: if reserved_prefix && c.name == DATA_SCHEMA_TABLE_ID_COLUMN_NAME {
190 ReservedColumnId::table_id()
191 } else if reserved_prefix && c.name == DATA_SCHEMA_TSID_COLUMN_NAME {
192 ReservedColumnId::tsid()
193 } else {
194 i as u32
195 },
196 }
197 })
198 .collect::<Vec<_>>();
199
200 let primary_key = create_table_expr
201 .primary_keys
202 .iter()
203 .map(|key| {
204 column_defs
205 .iter()
206 .find_map(|c| {
207 c.column_def.as_ref().and_then(|x| {
208 if &x.name == key {
209 Some(c.column_id)
210 } else {
211 None
212 }
213 })
214 })
215 .context(error::PrimaryKeyNotFoundSnafu { key })
216 })
217 .collect::<Result<_>>()?;
218
219 let template = CreateRequest {
220 region_id: 0,
221 engine: create_table_expr.engine.clone(),
222 column_defs,
223 primary_key,
224 path: String::new(),
225 options: create_table_expr.table_options.clone(),
226 partition: None,
227 requirements: None,
228 };
229
230 Ok(template)
231}
232
233pub struct CreateRequestBuilder {
235 template: CreateRequest,
236 physical_table_id: Option<TableId>,
238 requirements: RegionRequirements,
239}
240
241impl CreateRequestBuilder {
242 pub fn new(template: CreateRequest, physical_table_id: Option<TableId>) -> Self {
243 Self {
244 template,
245 physical_table_id,
246 requirements: RegionRequirements::empty(),
247 }
248 }
249
250 pub fn template(&self) -> &CreateRequest {
251 &self.template
252 }
253
254 pub fn with_requirements(mut self, requirements: RegionRequirements) -> Self {
255 self.requirements = requirements;
256 self
257 }
258
259 pub fn build_one(
260 &self,
261 region_id: RegionId,
262 storage_path: String,
263 region_wal_options: &RegionWalOptions,
264 partition_exprs: &HashMap<RegionNumber, String>,
265 ) -> Result<CreateRequest> {
266 let mut request = self.template.clone();
267
268 request.region_id = region_id.as_u64();
269 request.path = storage_path;
270 request.requirements = Some(self.requirements.into());
271 serialize_wal_options(&mut request.options, region_id, region_wal_options)
273 .context(SerializeWalOptionsSnafu { region_id })?;
274 request.partition = Some(prepare_partition_expr(region_id, partition_exprs));
275
276 if let Some(physical_table_id) = self.physical_table_id {
277 let physical_region_id = RegionId::new(physical_table_id, region_id.region_number());
281
282 request.options.insert(
283 LOGICAL_TABLE_METADATA_KEY.to_string(),
284 physical_region_id.as_u64().to_string(),
285 );
286 }
287
288 Ok(request)
289 }
290}
291
292fn prepare_partition_expr(
293 region_id: RegionId,
294 partition_exprs: &HashMap<RegionNumber, String>,
295) -> Partition {
296 let expr = partition_exprs.get(®ion_id.region_number()).cloned();
297 if expr.is_none() {
298 warn!("region {} has no partition expr", region_id);
299 }
300
301 Partition {
302 expression: expr.unwrap_or_default(),
303 ..Default::default()
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use std::collections::HashMap;
310
311 use store_api::storage::{RegionId, RegionNumber};
312
313 use super::*;
314 use crate::key::test_utils;
315
316 #[test]
317 fn test_create_sparse_reserved_column_ids() {
318 let mut expr = CreateTableExpr {
319 engine: MITO_ENGINE.into(),
320 table_options: HashMap::from([(PRIMARY_KEY_ENCODING.into(), "sparse".into())]),
321 primary_keys: vec![
322 DATA_SCHEMA_TABLE_ID_COLUMN_NAME.into(),
323 DATA_SCHEMA_TSID_COLUMN_NAME.into(),
324 "job".into(),
325 ],
326 time_index: "ts".into(),
327 column_defs: [
328 ("ts", ColumnDataType::TimestampMillisecond),
329 (DATA_SCHEMA_TABLE_ID_COLUMN_NAME, ColumnDataType::Uint32),
330 (DATA_SCHEMA_TSID_COLUMN_NAME, ColumnDataType::Uint64),
331 ("job", ColumnDataType::String),
332 ]
333 .into_iter()
334 .map(|(name, data_type)| ColumnDef {
335 name: name.into(),
336 data_type: data_type as i32,
337 is_nullable: false,
338 ..Default::default()
339 })
340 .collect(),
341 ..Default::default()
342 };
343 let template = build_template(&expr).unwrap();
344 assert_eq!(
345 template.primary_key,
346 vec![ReservedColumnId::table_id(), ReservedColumnId::tsid(), 3]
347 );
348 assert_eq!(
349 template.column_defs[1].column_id,
350 ReservedColumnId::table_id()
351 );
352 assert_eq!(template.column_defs[2].column_id, ReservedColumnId::tsid());
353 expr.table_options.clear();
355 assert_eq!(build_template(&expr).unwrap().primary_key, vec![1, 2, 3]);
356 }
357
358 #[test]
359 fn test_build_one_sets_partition_expr_per_region() {
360 let template = CreateRequest {
362 region_id: 0,
363 engine: "mito".to_string(),
364 column_defs: vec![],
365 primary_key: vec![],
366 path: String::new(),
367 options: Default::default(),
368 partition: None,
369 requirements: None,
370 };
371 let builder = CreateRequestBuilder::new(template, None);
372
373 let mut partition_exprs: HashMap<RegionNumber, String> = HashMap::new();
374 let expr_a =
375 r#"{"Expr":{"lhs":{"Column":"a"},"op":"Eq","rhs":{"Value":{"UInt32":1}}}}"#.to_string();
376 partition_exprs.insert(0, expr_a.clone());
377
378 let r0 = builder
379 .build_one(
380 RegionId::new(42, 0),
381 "/p".to_string(),
382 &Default::default(),
383 &partition_exprs,
384 )
385 .unwrap();
386 assert_eq!(r0.partition.as_ref().unwrap().expression, expr_a);
387 assert_eq!(
388 r0.requirements.map(RegionRequirements::from),
389 Some(RegionRequirements::empty())
390 );
391 }
392
393 #[test]
394 fn test_build_one_sets_explicit_requirements() {
395 let template = CreateRequest {
396 region_id: 0,
397 engine: "mito".to_string(),
398 column_defs: vec![],
399 primary_key: vec![],
400 path: String::new(),
401 options: Default::default(),
402 partition: None,
403 requirements: None,
404 };
405 let builder = CreateRequestBuilder::new(template, None)
406 .with_requirements(RegionRequirements::object_storage());
407
408 let request = builder
409 .build_one(
410 RegionId::new(42, 0),
411 "/p".to_string(),
412 &Default::default(),
413 &Default::default(),
414 )
415 .unwrap();
416
417 assert_eq!(
418 request.requirements.map(RegionRequirements::from),
419 Some(RegionRequirements::object_storage())
420 );
421 }
422
423 #[test]
424 fn test_build_template_for_physical_table_primary_key_matches_indices() {
425 let mut table_info = test_utils::new_test_table_info(42);
426 table_info.meta.primary_key_indices = vec![0, 2];
427 table_info.meta.column_ids = vec![10, 20, 30];
428
429 let template = build_template_from_raw_table_info_for_physical_table(&table_info).unwrap();
430 assert_eq!(template.primary_key, vec![10, 30]);
431 }
432}