Skip to main content

pipeline/manager/
table.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::sync::Arc;
16
17use api::v1::value::ValueData;
18use api::v1::{
19    ColumnDataType, ColumnDef, ColumnSchema as PbColumnSchema, Row, RowInsertRequest,
20    RowInsertRequests, Rows, SemanticType,
21};
22use arrow::array::{Array, AsArray};
23use arrow::datatypes::TimestampNanosecondType;
24use common_query::OutputData;
25use common_recordbatch::util as record_util;
26use common_telemetry::{debug, info};
27use common_time::timestamp::{TimeUnit, Timestamp};
28use datafusion::datasource::DefaultTableSource;
29use datafusion::logical_expr::col;
30use datafusion_common::TableReference;
31use datafusion_expr::{DmlStatement, LogicalPlan};
32use datatypes::timestamp::TimestampNanosecond;
33use itertools::Itertools;
34use operator::insert::InserterRef;
35use operator::statement::StatementExecutorRef;
36use query::QueryEngineRef;
37use session::context::{QueryContextBuilder, QueryContextRef};
38use snafu::{OptionExt, ResultExt, ensure};
39use table::TableRef;
40use table::metadata::TableInfo;
41use table::table::adapter::DfTableProviderAdapter;
42
43use crate::error::{
44    BuildDfLogicalPlanSnafu, CastTypeSnafu, CollectRecordsSnafu, DataFrameSnafu, Error,
45    ExecuteInternalStatementSnafu, InsertPipelineSnafu, InvalidPipelineVersionSnafu,
46    MultiPipelineWithDiffSchemaSnafu, PipelineNotFoundSnafu, RecordBatchLenNotMatchSnafu, Result,
47};
48use crate::etl::{Content, Pipeline, parse_with_options};
49use crate::manager::pipeline_cache::{PipelineCache, PipelineContent};
50use crate::manager::{PipelineInfo, PipelineVersion};
51use crate::metrics::METRIC_PIPELINE_TABLE_FIND_COUNT;
52use crate::options::PipelineOptions;
53use crate::util::prepare_dataframe_conditions;
54
55pub(crate) const PIPELINE_TABLE_NAME: &str = "pipelines";
56pub(crate) const PIPELINE_TABLE_PIPELINE_NAME_COLUMN_NAME: &str = "name";
57const PIPELINE_TABLE_PIPELINE_SCHEMA_COLUMN_NAME: &str = "schema";
58const PIPELINE_TABLE_PIPELINE_CONTENT_TYPE_COLUMN_NAME: &str = "content_type";
59const PIPELINE_TABLE_PIPELINE_CONTENT_COLUMN_NAME: &str = "pipeline";
60pub(crate) const PIPELINE_TABLE_CREATED_AT_COLUMN_NAME: &str = "created_at";
61pub(crate) const EMPTY_SCHEMA_NAME: &str = "";
62
63/// PipelineTable is a table that stores the pipeline schema and content.
64/// Every catalog has its own pipeline table.
65pub struct PipelineTable {
66    inserter: InserterRef,
67    statement_executor: StatementExecutorRef,
68    table: TableRef,
69    query_engine: QueryEngineRef,
70    cache: PipelineCache,
71    options: PipelineOptions,
72}
73
74impl PipelineTable {
75    /// Create a new PipelineTable.
76    pub fn new(
77        inserter: InserterRef,
78        statement_executor: StatementExecutorRef,
79        table: TableRef,
80        query_engine: QueryEngineRef,
81        options: &PipelineOptions,
82    ) -> Self {
83        Self {
84            inserter,
85            statement_executor,
86            table,
87            query_engine,
88            cache: PipelineCache::new(options.cache_ttl),
89            options: options.clone(),
90        }
91    }
92
93    /// Build the schema for the pipeline table.
94    /// Returns the (time index, primary keys, column) definitions.
95    pub fn build_pipeline_schema() -> (String, Vec<String>, Vec<ColumnDef>) {
96        (
97            PIPELINE_TABLE_CREATED_AT_COLUMN_NAME.to_string(),
98            vec![
99                PIPELINE_TABLE_PIPELINE_SCHEMA_COLUMN_NAME.to_string(),
100                PIPELINE_TABLE_PIPELINE_NAME_COLUMN_NAME.to_string(),
101                PIPELINE_TABLE_PIPELINE_CONTENT_TYPE_COLUMN_NAME.to_string(),
102            ],
103            vec![
104                ColumnDef {
105                    name: PIPELINE_TABLE_PIPELINE_NAME_COLUMN_NAME.to_string(),
106                    data_type: ColumnDataType::String as i32,
107                    is_nullable: false,
108                    default_constraint: vec![],
109                    semantic_type: SemanticType::Tag as i32,
110                    comment: "".to_string(),
111                    datatype_extension: None,
112                    options: None,
113                },
114                ColumnDef {
115                    name: PIPELINE_TABLE_PIPELINE_SCHEMA_COLUMN_NAME.to_string(),
116                    data_type: ColumnDataType::String as i32,
117                    is_nullable: false,
118                    default_constraint: vec![],
119                    semantic_type: SemanticType::Tag as i32,
120                    comment: "".to_string(),
121                    datatype_extension: None,
122                    options: None,
123                },
124                ColumnDef {
125                    name: PIPELINE_TABLE_PIPELINE_CONTENT_TYPE_COLUMN_NAME.to_string(),
126                    data_type: ColumnDataType::String as i32,
127                    is_nullable: false,
128                    default_constraint: vec![],
129                    semantic_type: SemanticType::Tag as i32,
130                    comment: "".to_string(),
131                    datatype_extension: None,
132                    options: None,
133                },
134                ColumnDef {
135                    name: PIPELINE_TABLE_PIPELINE_CONTENT_COLUMN_NAME.to_string(),
136                    data_type: ColumnDataType::String as i32,
137                    is_nullable: false,
138                    default_constraint: vec![],
139                    semantic_type: SemanticType::Field as i32,
140                    comment: "".to_string(),
141                    datatype_extension: None,
142                    options: None,
143                },
144                ColumnDef {
145                    name: PIPELINE_TABLE_CREATED_AT_COLUMN_NAME.to_string(),
146                    data_type: ColumnDataType::TimestampNanosecond as i32,
147                    is_nullable: false,
148                    default_constraint: vec![],
149                    semantic_type: SemanticType::Timestamp as i32,
150                    comment: "".to_string(),
151                    datatype_extension: None,
152                    options: None,
153                },
154            ],
155        )
156    }
157
158    /// Build the column schemas for inserting a row into the pipeline table.
159    fn build_insert_column_schemas() -> Vec<PbColumnSchema> {
160        vec![
161            PbColumnSchema {
162                column_name: PIPELINE_TABLE_PIPELINE_NAME_COLUMN_NAME.to_string(),
163                datatype: ColumnDataType::String.into(),
164                semantic_type: SemanticType::Tag.into(),
165                ..Default::default()
166            },
167            PbColumnSchema {
168                column_name: PIPELINE_TABLE_PIPELINE_SCHEMA_COLUMN_NAME.to_string(),
169                datatype: ColumnDataType::String.into(),
170                semantic_type: SemanticType::Tag.into(),
171                ..Default::default()
172            },
173            PbColumnSchema {
174                column_name: PIPELINE_TABLE_PIPELINE_CONTENT_TYPE_COLUMN_NAME.to_string(),
175                datatype: ColumnDataType::String.into(),
176                semantic_type: SemanticType::Tag.into(),
177                ..Default::default()
178            },
179            PbColumnSchema {
180                column_name: PIPELINE_TABLE_PIPELINE_CONTENT_COLUMN_NAME.to_string(),
181                datatype: ColumnDataType::String.into(),
182                semantic_type: SemanticType::Field.into(),
183                ..Default::default()
184            },
185            PbColumnSchema {
186                column_name: PIPELINE_TABLE_CREATED_AT_COLUMN_NAME.to_string(),
187                datatype: ColumnDataType::TimestampNanosecond.into(),
188                semantic_type: SemanticType::Timestamp.into(),
189                ..Default::default()
190            },
191        ]
192    }
193
194    fn query_ctx(table_info: &TableInfo) -> QueryContextRef {
195        QueryContextBuilder::default()
196            .current_catalog(table_info.catalog_name.clone())
197            .current_schema(table_info.schema_name.clone())
198            .build()
199            .into()
200    }
201
202    /// Compile a pipeline from a string.
203    pub fn compile_pipeline(&self, pipeline: &str) -> Result<Pipeline> {
204        let yaml_content = Content::Yaml(pipeline);
205        parse_with_options(&yaml_content, &self.options)
206    }
207
208    /// Insert a pipeline into the pipeline table.
209    async fn insert_pipeline_to_pipeline_table(
210        &self,
211        name: &str,
212        content_type: &str,
213        pipeline: &str,
214    ) -> Result<Timestamp> {
215        let now = Timestamp::current_time(TimeUnit::Nanosecond);
216
217        let table_info = self.table.table_info();
218
219        let insert = RowInsertRequest {
220            table_name: PIPELINE_TABLE_NAME.to_string(),
221            rows: Some(Rows {
222                schema: Self::build_insert_column_schemas(),
223                rows: vec![Row {
224                    values: vec![
225                        ValueData::StringValue(name.to_string()).into(),
226                        ValueData::StringValue(EMPTY_SCHEMA_NAME.to_string()).into(),
227                        ValueData::StringValue(content_type.to_string()).into(),
228                        ValueData::StringValue(pipeline.to_string()).into(),
229                        ValueData::TimestampNanosecondValue(now.value()).into(),
230                    ],
231                }],
232            }),
233        };
234
235        let requests = RowInsertRequests {
236            inserts: vec![insert],
237        };
238
239        let output = self
240            .inserter
241            .handle_row_inserts(
242                requests,
243                Self::query_ctx(&table_info),
244                &self.statement_executor,
245                false,
246                false,
247            )
248            .await
249            .context(InsertPipelineSnafu)?;
250
251        info!(
252            "Insert pipeline success, name: {:?}, table: {:?}, output: {:?}",
253            name,
254            table_info.full_table_name(),
255            output
256        );
257
258        Ok(now)
259    }
260
261    /// Get a pipeline by name.
262    /// If the pipeline is not in the cache, it will be get from table and compiled and inserted into the cache.
263    pub async fn get_pipeline(
264        &self,
265        schema: &str,
266        name: &str,
267        input_version: PipelineVersion,
268    ) -> Result<Arc<Pipeline>> {
269        self.cache
270            .get_pipeline_with(schema, name, input_version, async {
271                let pipeline_content = self.get_pipeline_str(schema, name, input_version).await?;
272                Ok(Arc::new(self.compile_pipeline(&pipeline_content.content)?))
273            })
274            .await
275    }
276
277    /// Get a original pipeline by name.
278    /// If the pipeline is not in the cache, it will be get from table and compiled and inserted into the cache.
279    pub async fn get_pipeline_str(
280        &self,
281        schema: &str,
282        name: &str,
283        input_version: PipelineVersion,
284    ) -> Result<PipelineContent> {
285        self.cache
286            .get_pipeline_str_with(schema, name, input_version, async {
287                self.load_pipeline_str(schema, name, input_version).await
288            })
289            .await
290    }
291
292    async fn load_pipeline_str(
293        &self,
294        schema: &str,
295        name: &str,
296        input_version: PipelineVersion,
297    ) -> Result<PipelineContent> {
298        let mut pipeline_vec;
299        match self.find_pipeline(name, input_version).await {
300            Ok(p) => {
301                METRIC_PIPELINE_TABLE_FIND_COUNT
302                    .with_label_values(&["true"])
303                    .inc();
304                pipeline_vec = p;
305            }
306            Err(e) => {
307                match e {
308                    Error::CollectRecords { .. } => {
309                        // if collect records failed, it means the pipeline table is temporary invalid
310                        // we should use failover cache
311                        METRIC_PIPELINE_TABLE_FIND_COUNT
312                            .with_label_values(&["false"])
313                            .inc();
314                        return self
315                            .cache
316                            .get_failover_cache(schema, name, input_version)
317                            .await?
318                            .context(PipelineNotFoundSnafu {
319                                name,
320                                version: input_version,
321                            });
322                    }
323                    _ => {
324                        // if other error, we should return it
325                        return Err(e);
326                    }
327                }
328            }
329        };
330        ensure!(
331            !pipeline_vec.is_empty(),
332            PipelineNotFoundSnafu {
333                name,
334                version: input_version
335            }
336        );
337
338        // if the result is exact one, use it
339        if pipeline_vec.len() == 1 {
340            let pipeline_content = pipeline_vec.remove(0);
341
342            self.cache
343                .insert_failover_cache(pipeline_content.clone(), input_version.is_none())
344                .await;
345            return Ok(pipeline_content);
346        }
347
348        // check if there's empty schema pipeline
349        // if there isn't, check current schema
350        let pipeline = pipeline_vec
351            .iter()
352            .position(|v| v.schema == EMPTY_SCHEMA_NAME)
353            .or_else(|| pipeline_vec.iter().position(|v| v.schema == schema))
354            .map(|idx| pipeline_vec.remove(idx));
355
356        // multiple pipeline with no empty or current schema
357        // throw an error
358        let pipeline_content = pipeline.with_context(|| MultiPipelineWithDiffSchemaSnafu {
359            name: name.to_string(),
360            current_schema: schema.to_string(),
361            schemas: pipeline_vec.iter().map(|v| v.schema.clone()).join(","),
362        })?;
363
364        self.cache
365            .insert_failover_cache(pipeline_content.clone(), input_version.is_none())
366            .await;
367        Ok(pipeline_content)
368    }
369
370    /// Insert a pipeline into the pipeline table and compile it.
371    /// Newly created pipelines will be saved under empty schema.
372    pub async fn insert_and_compile(
373        &self,
374        name: &str,
375        content_type: &str,
376        pipeline: &str,
377    ) -> Result<PipelineInfo> {
378        let compiled_pipeline = Arc::new(self.compile_pipeline(pipeline)?);
379        // we will use the version in the future
380        let version = self
381            .insert_pipeline_to_pipeline_table(name, content_type, pipeline)
382            .await?;
383
384        self.cache
385            .on_pipeline_created(PipelineContent {
386                name: name.to_string(),
387                content: pipeline.to_string(),
388                version: TimestampNanosecond(version),
389                schema: EMPTY_SCHEMA_NAME.to_string(),
390            })
391            .await;
392
393        Ok((version, compiled_pipeline))
394    }
395
396    pub async fn delete_pipeline(
397        &self,
398        name: &str,
399        version: PipelineVersion,
400    ) -> Result<Option<()>> {
401        // 0. version is ensured at the http api level not None
402        ensure!(
403            version.is_some(),
404            InvalidPipelineVersionSnafu { version: "None" }
405        );
406
407        // 1. check pipeline exist in catalog
408        let pipeline = self.find_pipeline(name, version).await?;
409        if pipeline.is_empty() {
410            return Ok(None);
411        }
412
413        // 2. prepare dataframe
414        let dataframe = self
415            .query_engine
416            .read_table(self.table.clone())
417            .context(DataFrameSnafu)?;
418
419        let dataframe = dataframe
420            .filter(prepare_dataframe_conditions(name, version))
421            .context(BuildDfLogicalPlanSnafu)?;
422
423        // 3. prepare dml stmt
424        let table_info = self.table.table_info();
425        let table_name = TableReference::full(
426            table_info.catalog_name.clone(),
427            table_info.schema_name.clone(),
428            table_info.name.clone(),
429        );
430
431        let table_provider = Arc::new(DfTableProviderAdapter::new(self.table.clone()));
432        let table_source = Arc::new(DefaultTableSource::new(table_provider));
433
434        // create dml stmt
435        let stmt = DmlStatement::new(
436            table_name,
437            table_source,
438            datafusion_expr::WriteOp::Delete,
439            Arc::new(dataframe.into_parts().1),
440        );
441
442        let plan = LogicalPlan::Dml(stmt);
443
444        // 4. execute dml stmt
445        let output = self
446            .query_engine
447            .execute(plan, Self::query_ctx(&table_info))
448            .await
449            .context(ExecuteInternalStatementSnafu)?;
450
451        info!(
452            "Delete pipeline success, name: {:?}, version: {:?}, table: {:?}, output: {:?}",
453            name,
454            version,
455            table_info.full_table_name(),
456            output
457        );
458
459        self.cache.invalidate(name, version).await;
460
461        Ok(Some(()))
462    }
463
464    // find all pipelines with name and version
465    // cloud be multiple with different schema
466    // return format: (pipeline content, schema, created_at)
467    async fn find_pipeline(
468        &self,
469        name: &str,
470        version: PipelineVersion,
471    ) -> Result<Vec<PipelineContent>> {
472        // 1. prepare dataframe
473        let dataframe = self
474            .query_engine
475            .read_table(self.table.clone())
476            .context(DataFrameSnafu)?;
477
478        // select all pipelines with name and version
479        let dataframe = dataframe
480            .filter(prepare_dataframe_conditions(name, version))
481            .context(BuildDfLogicalPlanSnafu)?
482            .select_columns(&[
483                PIPELINE_TABLE_PIPELINE_CONTENT_COLUMN_NAME,
484                PIPELINE_TABLE_PIPELINE_SCHEMA_COLUMN_NAME,
485                PIPELINE_TABLE_CREATED_AT_COLUMN_NAME,
486            ])
487            .context(BuildDfLogicalPlanSnafu)?
488            .sort(vec![
489                col(PIPELINE_TABLE_CREATED_AT_COLUMN_NAME).sort(false, true),
490            ])
491            .context(BuildDfLogicalPlanSnafu)?;
492
493        let plan = dataframe.into_parts().1;
494
495        let table_info = self.table.table_info();
496
497        debug!("find_pipeline_by_name: plan: {:?}", plan);
498
499        // 2. execute plan
500        let output = self
501            .query_engine
502            .execute(plan, Self::query_ctx(&table_info))
503            .await
504            .context(ExecuteInternalStatementSnafu)?;
505        let output = output
506            .map_dictionary_to_values()
507            .context(CollectRecordsSnafu)?;
508        let stream = match output.data {
509            OutputData::Stream(stream) => stream,
510            OutputData::RecordBatches(record_batches) => record_batches.as_stream(),
511            _ => unreachable!(),
512        };
513
514        // 3. construct result
515        let records = record_util::collect(stream)
516            .await
517            .context(CollectRecordsSnafu)?;
518
519        if records.is_empty() {
520            return Ok(vec![]);
521        }
522
523        ensure!(
524            !records.is_empty() && records.iter().all(|r| r.num_columns() == 3),
525            PipelineNotFoundSnafu { name, version }
526        );
527
528        let mut re = Vec::with_capacity(records.len());
529        for r in records {
530            let pipeline_content_column = r.column(0);
531            let pipeline_content = pipeline_content_column
532                .as_string_opt::<i32>()
533                .with_context(|| CastTypeSnafu {
534                    msg: format!(
535                        "can't downcast {:?} array into string vector",
536                        pipeline_content_column.data_type()
537                    ),
538                })?;
539
540            let pipeline_schema_column = r.column(1);
541            let pipeline_schema =
542                pipeline_schema_column
543                    .as_string_opt::<i32>()
544                    .with_context(|| CastTypeSnafu {
545                        msg: format!(
546                            "expecting pipeline schema column of type string, actual: {}",
547                            pipeline_schema_column.data_type()
548                        ),
549                    })?;
550
551            let pipeline_created_at_column = r.column(2);
552            let pipeline_created_at = pipeline_created_at_column
553                .as_primitive_opt::<TimestampNanosecondType>()
554                .with_context(|| CastTypeSnafu {
555                    msg: format!(
556                        "can't downcast {:?} array into scalar vector",
557                        pipeline_created_at_column.data_type()
558                    ),
559                })?;
560
561            debug!(
562                "find_pipeline_by_name: pipeline_content: {:?}, pipeline_schema: {:?}, pipeline_created_at: {:?}",
563                pipeline_content, pipeline_schema, pipeline_created_at
564            );
565
566            ensure!(
567                pipeline_content.len() == pipeline_schema.len()
568                    && pipeline_schema.len() == pipeline_created_at.len(),
569                RecordBatchLenNotMatchSnafu
570            );
571
572            let len = pipeline_content.len();
573            for i in 0..len {
574                re.push(PipelineContent {
575                    name: name.to_string(),
576                    content: pipeline_content.value(i).to_string(),
577                    version: TimestampNanosecond::new(pipeline_created_at.value(i)),
578                    schema: pipeline_schema.value(i).to_string(),
579                });
580            }
581        }
582
583        Ok(re)
584    }
585}