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