Skip to main content

frontend/instance/
dashboard.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::collections::HashMap;
16use std::sync::Arc;
17
18use api::v1::value::ValueData;
19use api::v1::{
20    ColumnDataType, ColumnDef, ColumnSchema as PbColumnSchema, Row, RowInsertRequest,
21    RowInsertRequests, Rows, SemanticType,
22};
23use async_trait::async_trait;
24use auth::{DASHBOARD_DELETE, DASHBOARD_QUERY, DASHBOARD_SAVE, PermissionReq};
25use common_catalog::consts::{DEFAULT_PRIVATE_SCHEMA_NAME, default_engine};
26use common_error::ext::BoxedError;
27use common_meta::rpc::ddl::TriggerReason;
28use common_query::OutputData;
29use common_recordbatch::util as record_util;
30use common_telemetry::info;
31use common_time::FOREVER;
32use datafusion::datasource::DefaultTableSource;
33use datafusion::logical_expr::col;
34use datafusion::sql::TableReference;
35use datafusion_expr::{DmlStatement, LogicalPlan, lit};
36use datatypes::arrow::array::{Array, AsArray};
37use servers::error::{
38    CollectRecordbatchSnafu, DataFusionSnafu, ExecuteQuerySnafu, NotSupportedSnafu,
39    TableNotFoundSnafu,
40};
41use servers::query_handler::DashboardDefinition;
42use session::context::{QueryContextBuilder, QueryContextRef};
43use snafu::{OptionExt, ResultExt};
44use table::TableRef;
45use table::metadata::TableInfo;
46use table::requests::TTL_KEY;
47use table::table::adapter::DfTableProviderAdapter;
48
49use crate::instance::Instance;
50
51pub const DASHBOARD_TABLE_NAME: &str = "dashboard";
52pub const DASHBOARD_TABLE_NAME_COLUMN_NAME: &str = "name";
53pub const DASHBOARD_TABLE_DEFINITION_COLUMN_NAME: &str = "definition";
54pub const DASHBOARD_TABLE_CREATED_AT_COLUMN_NAME: &str = "created_at";
55
56impl Instance {
57    /// Build a schema for dashboard table.
58    /// Returns the (time index, primary keys, column) definitions.
59    fn build_dashboard_schema() -> (String, Vec<String>, Vec<ColumnDef>) {
60        (
61            DASHBOARD_TABLE_CREATED_AT_COLUMN_NAME.to_string(),
62            vec![DASHBOARD_TABLE_NAME_COLUMN_NAME.to_string()],
63            vec![
64                ColumnDef {
65                    name: DASHBOARD_TABLE_NAME_COLUMN_NAME.to_string(),
66                    data_type: ColumnDataType::String as i32,
67                    is_nullable: false,
68                    default_constraint: vec![],
69                    semantic_type: SemanticType::Tag as i32,
70                    comment: String::new(),
71                    datatype_extension: None,
72                    options: None,
73                },
74                ColumnDef {
75                    name: DASHBOARD_TABLE_DEFINITION_COLUMN_NAME.to_string(),
76                    data_type: ColumnDataType::String as i32,
77                    is_nullable: false,
78                    default_constraint: vec![],
79                    semantic_type: SemanticType::Field as i32,
80                    comment: String::new(),
81                    datatype_extension: None,
82                    options: None,
83                },
84                ColumnDef {
85                    name: DASHBOARD_TABLE_CREATED_AT_COLUMN_NAME.to_string(),
86                    data_type: ColumnDataType::TimestampNanosecond as i32,
87                    is_nullable: false,
88                    default_constraint: vec![],
89                    semantic_type: SemanticType::Timestamp as i32,
90                    comment: String::new(),
91                    datatype_extension: None,
92                    options: None,
93                },
94            ],
95        )
96    }
97
98    /// Build a column schemas for inserting a row into the dashboard table.
99    fn build_dashboard_insert_column_schemas() -> Vec<PbColumnSchema> {
100        vec![
101            PbColumnSchema {
102                column_name: DASHBOARD_TABLE_NAME_COLUMN_NAME.to_string(),
103                datatype: ColumnDataType::String.into(),
104                semantic_type: SemanticType::Tag.into(),
105                ..Default::default()
106            },
107            PbColumnSchema {
108                column_name: DASHBOARD_TABLE_DEFINITION_COLUMN_NAME.to_string(),
109                datatype: ColumnDataType::String.into(),
110                semantic_type: SemanticType::Field.into(),
111                ..Default::default()
112            },
113            PbColumnSchema {
114                column_name: DASHBOARD_TABLE_CREATED_AT_COLUMN_NAME.to_string(),
115                datatype: ColumnDataType::TimestampNanosecond.into(),
116                semantic_type: SemanticType::Timestamp.into(),
117                ..Default::default()
118            },
119        ]
120    }
121
122    fn dashboard_query_ctx(table_info: &TableInfo) -> QueryContextRef {
123        QueryContextBuilder::default()
124            .current_catalog(table_info.catalog_name.clone())
125            .current_schema(table_info.schema_name.clone())
126            .build()
127            .into()
128    }
129
130    async fn create_dashboard_table_if_not_exists(
131        &self,
132        ctx: QueryContextRef,
133    ) -> servers::error::Result<TableRef> {
134        let catalog = ctx.current_catalog();
135
136        if let Some(table) = self
137            .catalog_manager
138            .table(
139                catalog,
140                DEFAULT_PRIVATE_SCHEMA_NAME,
141                DASHBOARD_TABLE_NAME,
142                Some(&ctx),
143            )
144            .await?
145        {
146            return Ok(table);
147        }
148
149        let (time_index, primary_keys, column_defs) = Self::build_dashboard_schema();
150
151        let mut table_options = HashMap::new();
152        table_options.insert(TTL_KEY.to_string(), FOREVER.to_string());
153
154        let mut create_table_expr = api::v1::CreateTableExpr {
155            catalog_name: catalog.to_string(),
156            schema_name: DEFAULT_PRIVATE_SCHEMA_NAME.to_string(),
157            table_name: DASHBOARD_TABLE_NAME.to_string(),
158            desc: "GreptimeDB dashboard table".to_string(),
159            column_defs,
160            time_index,
161            primary_keys,
162            create_if_not_exists: true,
163            table_options,
164            table_id: None,
165            engine: default_engine().to_string(),
166        };
167
168        self.statement_executor
169            .create_table_inner(
170                &mut create_table_expr,
171                None,
172                ctx.clone(),
173                TriggerReason::AutoCreate,
174            )
175            .await
176            .map_err(BoxedError::new)
177            .context(ExecuteQuerySnafu)?;
178
179        let table = self
180            .catalog_manager
181            .table(
182                catalog,
183                DEFAULT_PRIVATE_SCHEMA_NAME,
184                DASHBOARD_TABLE_NAME,
185                Some(&ctx),
186            )
187            .await?
188            .context(TableNotFoundSnafu {
189                catalog: catalog.to_string(),
190                schema: DEFAULT_PRIVATE_SCHEMA_NAME.to_string(),
191                table: DASHBOARD_TABLE_NAME.to_string(),
192            })?;
193
194        Ok(table)
195    }
196
197    /// Insert a dashboard into the dashboard table.
198    async fn insert_dashboard(
199        &self,
200        name: &str,
201        definition: &str,
202        query_ctx: QueryContextRef,
203    ) -> servers::error::Result<()> {
204        let table = self
205            .create_dashboard_table_if_not_exists(query_ctx.clone())
206            .await?;
207        let table_info = table.table_info();
208
209        let insert = RowInsertRequest {
210            table_name: DASHBOARD_TABLE_NAME.to_string(),
211            rows: Some(Rows {
212                schema: Self::build_dashboard_insert_column_schemas(),
213                rows: vec![Row {
214                    values: vec![
215                        ValueData::StringValue(name.to_string()).into(),
216                        ValueData::StringValue(definition.to_string()).into(),
217                        ValueData::TimestampNanosecondValue(0).into(),
218                    ],
219                }],
220            }),
221        };
222
223        let requests = RowInsertRequests {
224            inserts: vec![insert],
225        };
226
227        let output = self
228            .inserter
229            .handle_row_inserts(
230                requests,
231                Self::dashboard_query_ctx(&table_info),
232                &self.statement_executor,
233                false,
234                false,
235            )
236            .await
237            .map_err(BoxedError::new)
238            .context(ExecuteQuerySnafu)?;
239
240        info!(
241            "Insert dashboard success, name: {}, table: {}, output: {:?}",
242            name,
243            table_info.full_table_name(),
244            output
245        );
246
247        Ok(())
248    }
249
250    /// List all dashboards.
251    async fn list_dashboards(
252        &self,
253        query_ctx: QueryContextRef,
254    ) -> servers::error::Result<Vec<DashboardDefinition>> {
255        let table = if let Some(table) = self
256            .catalog_manager
257            .table(
258                query_ctx.current_catalog(),
259                DEFAULT_PRIVATE_SCHEMA_NAME,
260                DASHBOARD_TABLE_NAME,
261                Some(&query_ctx),
262            )
263            .await?
264        {
265            table
266        } else {
267            return Ok(vec![]);
268        };
269
270        let table_info = table.table_info();
271
272        let dataframe = self
273            .query_engine
274            .read_table(table.clone())
275            .map_err(BoxedError::new)
276            .context(ExecuteQuerySnafu)?;
277
278        let dataframe = dataframe
279            .select_columns(&[
280                DASHBOARD_TABLE_NAME_COLUMN_NAME,
281                DASHBOARD_TABLE_DEFINITION_COLUMN_NAME,
282            ])
283            .context(DataFusionSnafu)?;
284
285        let plan = dataframe.into_parts().1;
286
287        let output = self
288            .query_engine
289            .execute(plan, Self::dashboard_query_ctx(&table_info))
290            .await
291            .map_err(BoxedError::new)
292            .context(ExecuteQuerySnafu)?;
293        let output = output
294            .map_dictionary_to_values()
295            .context(CollectRecordbatchSnafu)?;
296
297        let stream = match output.data {
298            OutputData::Stream(stream) => stream,
299            OutputData::RecordBatches(record_batches) => record_batches.as_stream(),
300            _ => unreachable!(),
301        };
302
303        let records = record_util::collect(stream)
304            .await
305            .context(CollectRecordbatchSnafu)?;
306
307        let mut dashboards = Vec::new();
308
309        for r in &records {
310            let name_column = r.column(0);
311            let definition_column = r.column(1);
312
313            let name = name_column
314                .as_string_opt::<i32>()
315                .context(NotSupportedSnafu {
316                    feat: "Invalid data type for greptime_private.dashboard.name",
317                })?;
318
319            let definition =
320                definition_column
321                    .as_string_opt::<i32>()
322                    .context(NotSupportedSnafu {
323                        feat: "Invalid data type for greptime_private.dashboard.definition",
324                    })?;
325
326            for i in 0..name.len() {
327                dashboards.push(DashboardDefinition {
328                    name: name.value(i).to_string(),
329                    definition: definition.value(i).to_string(),
330                });
331            }
332        }
333
334        Ok(dashboards)
335    }
336
337    /// Delete a dashboard by name.
338    async fn delete_dashboard(
339        &self,
340        name: &str,
341        query_ctx: QueryContextRef,
342    ) -> servers::error::Result<()> {
343        let table = self
344            .create_dashboard_table_if_not_exists(query_ctx.clone())
345            .await?;
346        let table_info = table.table_info();
347
348        let dataframe = self
349            .query_engine
350            .read_table(table.clone())
351            .map_err(BoxedError::new)
352            .context(ExecuteQuerySnafu)?;
353
354        let name_condition = col(DASHBOARD_TABLE_NAME_COLUMN_NAME).eq(lit(name));
355
356        let dataframe = dataframe.filter(name_condition).context(DataFusionSnafu)?;
357
358        let table_name = TableReference::full(
359            table_info.catalog_name.clone(),
360            table_info.schema_name.clone(),
361            table_info.name.clone(),
362        );
363
364        let table_provider = Arc::new(DfTableProviderAdapter::new(table.clone()));
365        let table_source = Arc::new(DefaultTableSource::new(table_provider));
366
367        let stmt = DmlStatement::new(
368            table_name,
369            table_source,
370            datafusion_expr::WriteOp::Delete,
371            Arc::new(dataframe.into_parts().1),
372        );
373
374        let plan = LogicalPlan::Dml(stmt);
375
376        let output = self
377            .query_engine
378            .execute(plan, Self::dashboard_query_ctx(&table_info))
379            .await
380            .map_err(BoxedError::new)
381            .context(ExecuteQuerySnafu)?;
382
383        info!(
384            "Delete dashboard success, name: {}, table: {}, output: {:?}",
385            name,
386            table_info.full_table_name(),
387            output
388        );
389
390        Ok(())
391    }
392}
393
394#[async_trait]
395impl servers::query_handler::DashboardHandler for Instance {
396    async fn save(
397        &self,
398        name: &str,
399        definition: &str,
400        ctx: QueryContextRef,
401    ) -> servers::error::Result<()> {
402        self.check_permission(&ctx, PermissionReq::Action(DASHBOARD_SAVE))?;
403        self.insert_dashboard(name, definition, ctx).await
404    }
405
406    async fn list(&self, ctx: QueryContextRef) -> servers::error::Result<Vec<DashboardDefinition>> {
407        self.check_permission(&ctx, PermissionReq::Action(DASHBOARD_QUERY))?;
408        self.list_dashboards(ctx).await
409    }
410
411    async fn delete(&self, name: &str, ctx: QueryContextRef) -> servers::error::Result<()> {
412        self.check_permission(&ctx, PermissionReq::Action(DASHBOARD_DELETE))?;
413        self.delete_dashboard(name, ctx).await
414    }
415}