Skip to main content

risingwave_frontend/handler/
create_index.rs

1// Copyright 2022 RisingWave Labs
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, HashSet};
16use std::num::NonZeroU32;
17use std::sync::Arc;
18
19use either::Either;
20use fixedbitset::FixedBitSet;
21use itertools::Itertools;
22use pgwire::pg_response::{PgResponse, StatementType};
23use risingwave_common::catalog::{IndexId, TableId};
24use risingwave_common::types::DataType;
25use risingwave_common::util::recursive::{Recurse, tracker};
26use risingwave_common::util::sort_util::{ColumnOrder, OrderType};
27use risingwave_pb::catalog::{
28    CreateType, PbFlatIndexConfig, PbHnswFlatIndexConfig, PbIndex, PbIndexColumnProperties,
29    PbStreamJobStatus, PbVectorIndexInfo,
30};
31use risingwave_pb::common::PbDistanceType;
32use risingwave_pb::expr::expr_node::RexNode;
33use risingwave_sqlparser::ast;
34use risingwave_sqlparser::ast::{Ident, ObjectName, OrderByExpr};
35use thiserror_ext::AsReport;
36
37use super::RwPgResponse;
38use crate::TableCatalog;
39use crate::binder::Binder;
40use crate::catalog::root_catalog::SchemaPath;
41use crate::catalog::{DatabaseId, SchemaId};
42use crate::error::{ErrorCode, Result};
43use crate::expr::{Expr, ExprImpl, ExprRewriter, ExprVisitor, InputRef, is_impure_func_call};
44use crate::handler::HandlerArgs;
45use crate::handler::create_mv::resolve_streaming_job_resource_type;
46use crate::handler::util::{
47    LongRunningNotificationAction, execute_with_long_running_notification,
48    reject_internal_table_dependency,
49};
50use crate::optimizer::plan_expr_rewriter::ConstEvalRewriter;
51use crate::optimizer::plan_node::utils::plan_can_use_background_ddl;
52use crate::optimizer::plan_node::{
53    Explain, LogicalProject, LogicalScan, StreamMaterialize, StreamPlanRef as PlanRef,
54    ensure_sync_log_store_fragment_root,
55};
56use crate::optimizer::property::{Distribution, Order, RequiredDist};
57use crate::optimizer::{LogicalPlanRoot, OptimizerContext, OptimizerContextRef, PlanRoot};
58use crate::scheduler::streaming_manager::CreatingStreamingJobInfo;
59use crate::session::SessionImpl;
60use crate::session::current::notice_to_user;
61use crate::stream_fragmenter::{GraphJobType, build_graph};
62
63pub(crate) fn resolve_index_schema(
64    session: &SessionImpl,
65    index_name: ObjectName,
66    table_name: ObjectName,
67) -> Result<(String, Arc<TableCatalog>, String)> {
68    let db_name = &session.database();
69    let (schema_name, table_name) = Binder::resolve_schema_qualified_name(db_name, &table_name)?;
70    let search_path = session.config().search_path();
71    let user_name = &session.user_name();
72    let schema_path = SchemaPath::new(schema_name.as_deref(), &search_path, user_name);
73
74    let index_table_name = Binder::resolve_index_name(index_name)?;
75
76    let catalog_reader = session.env().catalog_reader();
77    let read_guard = catalog_reader.read_guard();
78    let (table, schema_name) =
79        read_guard.get_created_table_by_name(db_name, schema_path, &table_name)?;
80    reject_internal_table_dependency(table.as_ref(), "CREATE INDEX")?;
81    Ok((schema_name.to_owned(), table.clone(), index_table_name))
82}
83
84pub(crate) struct IndexColumnExprValidator {
85    allow_impure: bool,
86    result: Result<()>,
87}
88
89impl IndexColumnExprValidator {
90    fn unsupported_expr_err(expr: &ExprImpl) -> ErrorCode {
91        ErrorCode::NotSupported(
92            format!("unsupported index column expression type: {:?}", expr),
93            "use columns or expressions instead".into(),
94        )
95    }
96
97    pub(crate) fn validate(expr: &ExprImpl, allow_impure: bool) -> Result<()> {
98        match expr {
99            ExprImpl::InputRef(_) | ExprImpl::FunctionCall(_) => {}
100            other_expr => {
101                return Err(Self::unsupported_expr_err(other_expr).into());
102            }
103        }
104        let mut visitor = Self {
105            allow_impure,
106            result: Ok(()),
107        };
108        visitor.visit_expr(expr);
109        visitor.result
110    }
111}
112
113impl ExprVisitor for IndexColumnExprValidator {
114    fn visit_expr(&mut self, expr: &ExprImpl) {
115        if self.result.is_err() {
116            return;
117        }
118        tracker!().recurse(|t| {
119            if t.depth_reaches(crate::expr::EXPR_DEPTH_THRESHOLD) {
120                notice_to_user(crate::expr::EXPR_TOO_DEEP_NOTICE);
121            }
122
123            match expr {
124                ExprImpl::InputRef(_) | ExprImpl::Literal(_) => {}
125                ExprImpl::FunctionCall(inner) => {
126                    if !self.allow_impure && is_impure_func_call(inner) {
127                        self.result = Err(ErrorCode::NotSupported(
128                            "this expression is impure".into(),
129                            "use a pure expression instead".into(),
130                        )
131                        .into());
132                        return;
133                    }
134                    self.visit_function_call(inner)
135                }
136                other_expr => {
137                    self.result = Err(Self::unsupported_expr_err(other_expr).into());
138                }
139            }
140        })
141    }
142}
143
144pub(crate) fn gen_create_index_plan(
145    session: &SessionImpl,
146    context: OptimizerContextRef,
147    schema_name: String,
148    table: Arc<TableCatalog>,
149    index_table_name: String,
150    method: Option<Ident>,
151    columns: Vec<OrderByExpr>,
152    include: Vec<Ident>,
153    distributed_by: Vec<ast::Expr>,
154) -> Result<(PlanRef, TableCatalog, PbIndex)> {
155    if table.is_index() {
156        return Err(
157            ErrorCode::InvalidInputSyntax(format!("\"{}\" is an index", table.name)).into(),
158        );
159    }
160
161    // A relation without primary key holds at most one row, e.g. a materialized view of a global
162    // aggregation. The index is still created for compatibility, but the planner will never
163    // prefer it over the primary table.
164    if table.pk().is_empty() {
165        session.notice_to_user(format!(
166            "index \"{}\" will never be used by the planner: \"{}\" has no primary key and holds at most one row",
167            index_table_name, table.name
168        ));
169    }
170
171    if !session.is_super_user() && session.user_id() != table.owner {
172        return Err(ErrorCode::PermissionDenied(format!(
173            "must be owner of table \"{}\"",
174            table.name
175        ))
176        .into());
177    }
178
179    let vector_index_config = if let Some(method) = method {
180        match method.real_value().to_ascii_lowercase().as_str() {
181            "default" => None,
182            "flat" => Some(risingwave_pb::catalog::vector_index_info::PbConfig::Flat(
183                PbFlatIndexConfig {},
184            )),
185            "hnsw" => {
186                let with_options = context.with_options();
187                let parse_non_zero_u32 = |key: &str, default| {
188                    with_options
189                        .get(key)
190                        .map(|v| {
191                            let v = v.parse::<u32>().map_err(|e| {
192                                ErrorCode::InvalidInputSyntax(format!(
193                                    "invalid {} value {}: failed to parse {}",
194                                    key,
195                                    v,
196                                    e.as_report()
197                                ))
198                            })?;
199                            if v == 0 {
200                                Err(ErrorCode::InvalidInputSyntax(format!(
201                                    "invalid {} value {}: should not be zero",
202                                    key, v
203                                )))
204                            } else {
205                                Ok(v)
206                            }
207                        })
208                        .transpose()
209                        .map(|v| v.unwrap_or(default))
210                };
211                Some(
212                    risingwave_pb::catalog::vector_index_info::PbConfig::HnswFlat(
213                        PbHnswFlatIndexConfig {
214                            m: parse_non_zero_u32("m", 16)?, /* default value borrowed from pg_vector */
215                            ef_construction: parse_non_zero_u32("ef_construction", 64)?, /* default value borrowed from pg_vector */
216                            max_level: parse_non_zero_u32("max_level", 10)?,
217                        },
218                    ),
219                )
220            }
221            _ => {
222                return Err(ErrorCode::InvalidInputSyntax(format!(
223                    "invalid index method {}",
224                    method
225                ))
226                .into());
227            }
228        }
229    } else {
230        None
231    };
232
233    let is_vector_index = vector_index_config.is_some();
234
235    if is_vector_index && !table.append_only {
236        return Err(ErrorCode::InvalidInputSyntax(format!(
237            "\"{}\" is not append-only but vector index must be append-only",
238            table.name
239        ))
240        .into());
241    }
242
243    let mut binder = Binder::new_for_stream(session);
244    binder.bind_table(Some(&schema_name), &table.name)?;
245
246    let mut index_columns_ordered_expr = vec![];
247    let mut include_columns_expr = vec![];
248    let mut distributed_columns_expr = vec![];
249    if is_vector_index && columns.len() != 1 {
250        return Err(ErrorCode::InvalidInputSyntax(format!(
251            "vector index must be defined on exactly 1 column, but got {}",
252            columns.len()
253        ))
254        .into());
255    }
256    let mut dimension = None;
257    for column in columns {
258        if is_vector_index && (column.asc.is_some() || column.nulls_first.is_some()) {
259            return Err(ErrorCode::InvalidInputSyntax(
260                "vector index cannot specify order".to_owned(),
261            )
262            .into());
263        }
264        let order_type = OrderType::from_bools(column.asc, column.nulls_first);
265        let expr_impl = binder.bind_expr(&column.expr)?;
266        // Do constant folding and timezone transportation on expressions so that batch queries can match it in the same form.
267        let mut const_eval = ConstEvalRewriter { error: None };
268        let expr_impl = const_eval.rewrite_expr(expr_impl);
269        let expr_impl = context.session_timezone().rewrite_expr(expr_impl);
270        let allow_impure = is_vector_index;
271        IndexColumnExprValidator::validate(&expr_impl, allow_impure)?;
272        if is_vector_index {
273            match expr_impl.return_type() {
274                DataType::Vector(d) => {
275                    dimension = Some(d);
276                }
277                other => {
278                    return Err(ErrorCode::InvalidInputSyntax(format!(
279                        "vector index must be defined on column of Vector type, but got {:?}",
280                        other
281                    ))
282                    .into());
283                }
284            }
285        }
286        index_columns_ordered_expr.push((expr_impl, order_type));
287    }
288
289    if include.is_empty() {
290        // Create index to include all (non-hidden) columns by default.
291        include_columns_expr = table
292            .columns()
293            .iter()
294            .enumerate()
295            .filter(|(_, column)| !column.is_hidden)
296            .map(|(x, column)| {
297                ExprImpl::InputRef(InputRef::new(x, column.column_desc.data_type.clone()).into())
298            })
299            .collect_vec();
300    } else {
301        for column in include {
302            let expr_impl =
303                binder.bind_expr(&risingwave_sqlparser::ast::Expr::Identifier(column))?;
304            include_columns_expr.push(expr_impl);
305        }
306    };
307
308    if is_vector_index && !distributed_by.is_empty() {
309        return Err(ErrorCode::InvalidInputSyntax(
310            "vector index cannot define DISTRIBUTED BY".to_owned(),
311        )
312        .into());
313    }
314
315    for column in distributed_by {
316        let expr_impl = binder.bind_expr(&column)?;
317        distributed_columns_expr.push(expr_impl);
318    }
319
320    // Remove duplicate column of index columns
321    let mut set = HashSet::new();
322    index_columns_ordered_expr = index_columns_ordered_expr
323        .into_iter()
324        .filter(|(expr, _)| match expr {
325            ExprImpl::InputRef(input_ref) => set.insert(input_ref.index),
326            ExprImpl::FunctionCall(_) => true,
327            _ => unreachable!(),
328        })
329        .collect_vec();
330
331    // Remove include columns are already in index columns
332    include_columns_expr = include_columns_expr
333        .into_iter()
334        .filter(|expr| match expr {
335            ExprImpl::InputRef(input_ref) => set.insert(input_ref.index),
336            _ => unreachable!(),
337        })
338        .collect_vec();
339
340    // Remove duplicate columns of distributed by columns
341    let mut set = HashSet::new();
342    let distributed_columns_expr = distributed_columns_expr
343        .into_iter()
344        .filter(|expr| match expr {
345            ExprImpl::InputRef(input_ref) => set.insert(input_ref.index),
346            ExprImpl::FunctionCall(_) => true,
347            _ => unreachable!(),
348        })
349        .collect_vec();
350    // Distributed by columns should be a prefix of index columns
351    if !index_columns_ordered_expr
352        .iter()
353        .map(|(expr, _)| expr.clone())
354        .collect_vec()
355        .starts_with(&distributed_columns_expr)
356    {
357        return Err(ErrorCode::InvalidInputSyntax(
358            "Distributed by columns should be a prefix of index columns".to_owned(),
359        )
360        .into());
361    }
362
363    let (index_database_id, index_schema_id) =
364        session.get_database_and_schema_id_for_create(Some(schema_name))?;
365
366    let (plan, mut index_table) = if let Some(vector_index_config) = vector_index_config {
367        assert!(distributed_columns_expr.is_empty());
368        assert_eq!(1, index_columns_ordered_expr.len());
369        let input = assemble_input(
370            table.clone(),
371            context.clone(),
372            &index_columns_ordered_expr,
373            &include_columns_expr,
374            RequiredDist::PhysicalDist(Distribution::Single),
375        )?;
376        let definition = context.normalized_sql().to_owned();
377        let retention_seconds = table.retention_seconds.and_then(NonZeroU32::new);
378
379        let distance_type = match context
380            .with_options()
381            .get("distance_type")
382            .ok_or_else(|| {
383                ErrorCode::InvalidInputSyntax(
384                    "vector index missing `distance_type` in with options".to_owned(),
385                )
386            })?
387            .to_ascii_lowercase()
388            .as_str()
389        {
390            "l1" => PbDistanceType::L1,
391            "l2" => PbDistanceType::L2Sqr,
392            "inner_product" => PbDistanceType::InnerProduct,
393            "cosine" => PbDistanceType::Cosine,
394            other => {
395                return Err(ErrorCode::InvalidInputSyntax(format!(
396                    "unsupported vector index distance type: {}",
397                    other
398                ))
399                .into());
400            }
401        };
402
403        let vector_index_write = input.gen_vector_index_plan(
404            index_table_name.clone(),
405            index_database_id,
406            index_schema_id,
407            definition,
408            retention_seconds,
409            PbVectorIndexInfo {
410                dimension: dimension.expect("should be set for vector index") as _,
411                config: Some(vector_index_config),
412                distance_type: distance_type as _,
413            },
414        )?;
415        let index_table = vector_index_write.table().clone();
416        let plan: PlanRef = ensure_sync_log_store_fragment_root(vector_index_write.into());
417        (plan, index_table)
418    } else {
419        // Manually assemble the materialization plan for the index MV.
420        let materialize = assemble_materialize(
421            index_database_id,
422            index_schema_id,
423            table.clone(),
424            context.clone(),
425            index_table_name.clone(),
426            &index_columns_ordered_expr,
427            &include_columns_expr,
428            // We use the first index column as distributed key by default if users
429            // haven't specified the distributed by columns.
430            if distributed_columns_expr.is_empty() {
431                1
432            } else {
433                distributed_columns_expr.len()
434            },
435        )?;
436        let index_table = materialize.table().clone();
437        let plan: PlanRef = ensure_sync_log_store_fragment_root(materialize.into());
438        (plan, index_table)
439    };
440
441    {
442        // Inherit table properties
443        index_table.retention_seconds = table.retention_seconds;
444    }
445
446    index_table.owner = table.owner;
447
448    let index_columns_len = index_columns_ordered_expr.len() as u32;
449    let index_column_properties = index_columns_ordered_expr
450        .iter()
451        .map(|(_, order)| PbIndexColumnProperties {
452            is_desc: order.is_descending(),
453            nulls_first: order.nulls_are_first(),
454        })
455        .collect();
456    let index_item = build_index_item(
457        &index_table,
458        table.name(),
459        &table,
460        index_columns_ordered_expr,
461    );
462
463    // The index stores the stream key of the primary table, but the primary key can be wider, e.g.
464    // `[order_key..., stream_key...]` for an `ORDER BY` materialized view. Unless the remaining
465    // primary key columns are indexed or included, the index cannot locate rows of the primary
466    // table and only serves queries it fully covers.
467    let stored_primary_columns: HashSet<usize> = index_item
468        .iter()
469        // The first item of a vector index is the vector, not an info column.
470        .skip(if is_vector_index { 1 } else { 0 })
471        .filter_map(|item| match item.rex_node {
472            Some(RexNode::InputRef(column_index)) => Some(column_index as usize),
473            _ => None,
474        })
475        .collect();
476    let missing_pk_columns = table
477        .pk()
478        .iter()
479        .filter(|order| !stored_primary_columns.contains(&order.column_index))
480        .map(|order| format!("\"{}\"", table.columns()[order.column_index].name()))
481        .collect_vec();
482    if !missing_pk_columns.is_empty() {
483        session.notice_to_user(format!(
484            "index \"{}\" does not store primary key column(s) {} of \"{}\", so the planner can use it only for queries it fully covers; include these columns to let it look up rows of \"{}\"",
485            index_table_name,
486            missing_pk_columns.join(", "),
487            table.name,
488            table.name,
489        ));
490    }
491
492    let create_type =
493        if context.session_ctx().config().background_ddl() && plan_can_use_background_ddl(&plan) {
494            CreateType::Background
495        } else {
496            CreateType::Foreground
497        };
498
499    let index_prost = PbIndex {
500        id: IndexId::placeholder(),
501        schema_id: index_schema_id,
502        database_id: index_database_id,
503        name: index_table_name,
504        owner: index_table.owner,
505        index_table_id: TableId::placeholder(),
506        primary_table_id: table.id,
507        index_item,
508        index_column_properties,
509        index_columns_len,
510        initialized_at_epoch: None,
511        created_at_epoch: None,
512        stream_job_status: PbStreamJobStatus::Creating.into(),
513        initialized_at_cluster_version: None,
514        created_at_cluster_version: None,
515        create_type: create_type.into(),
516    };
517
518    let ctx = plan.ctx();
519    let explain_trace = ctx.is_explain_trace();
520    if explain_trace {
521        ctx.trace("Create Index:");
522        ctx.trace(plan.explain_to_string());
523    }
524
525    Ok((plan, index_table, index_prost))
526}
527
528fn build_index_item(
529    index_table: &TableCatalog,
530    primary_table_name: &str,
531    primary_table: &TableCatalog,
532    index_columns: Vec<(ExprImpl, OrderType)>,
533) -> Vec<risingwave_pb::expr::ExprNode> {
534    let primary_table_desc_map = primary_table
535        .columns
536        .iter()
537        .enumerate()
538        .map(|(x, y)| (y.name.clone(), x))
539        .collect::<HashMap<_, _>>();
540
541    let primary_table_name_prefix = format!("{}.", primary_table_name);
542
543    let index_columns_len = index_columns.len();
544    index_columns
545        .into_iter()
546        .map(|(expr, _)| expr.to_expr_proto())
547        .chain(
548            index_table
549                .columns
550                .iter()
551                .map(|c| &c.column_desc)
552                .skip(index_columns_len)
553                .map(|x| {
554                    let name = if x.name.starts_with(&primary_table_name_prefix) {
555                        x.name[primary_table_name_prefix.len()..].to_string()
556                    } else {
557                        x.name.clone()
558                    };
559
560                    let column_index = *primary_table_desc_map.get(&name).unwrap();
561                    InputRef {
562                        index: column_index,
563                        data_type: primary_table
564                            .columns
565                            .get(column_index)
566                            .unwrap()
567                            .data_type
568                            .clone(),
569                    }
570                    .to_expr_proto()
571                }),
572        )
573        .collect_vec()
574}
575
576fn assemble_input(
577    table_catalog: Arc<TableCatalog>,
578    context: OptimizerContextRef,
579    index_columns: &[(ExprImpl, OrderType)],
580    include_columns: &[ExprImpl],
581    required_dist: RequiredDist,
582) -> Result<LogicalPlanRoot> {
583    // Build logical plan and then call gen_create_index_plan
584    // LogicalProject(index_columns, include_columns)
585    //   LogicalScan(table_desc)
586
587    let logical_scan = LogicalScan::create(table_catalog.clone(), context, None);
588
589    let exprs = index_columns
590        .iter()
591        .map(|(expr, _)| expr.clone())
592        .chain(include_columns.iter().cloned())
593        .collect_vec();
594
595    let logical_project = LogicalProject::create(logical_scan.into(), exprs);
596    let mut project_required_cols = FixedBitSet::with_capacity(logical_project.schema().len());
597    project_required_cols.toggle_range(0..logical_project.schema().len());
598
599    let mut col_names = HashSet::new();
600    let mut count = 0;
601
602    let out_names: Vec<String> = index_columns
603        .iter()
604        .map(|(expr, _)| match expr {
605            ExprImpl::InputRef(input_ref) => table_catalog
606                .columns()
607                .get(input_ref.index)
608                .unwrap()
609                .name()
610                .to_owned(),
611            ExprImpl::FunctionCall(func) => {
612                let func_name = func.func_type().as_str_name().to_owned();
613                let mut name = func_name.clone();
614                while !col_names.insert(name.clone()) {
615                    count += 1;
616                    name = format!("{}{}", func_name, count);
617                }
618                name
619            }
620            _ => unreachable!(),
621        })
622        .chain(include_columns.iter().map(|expr| {
623            match expr {
624                ExprImpl::InputRef(input_ref) => table_catalog
625                    .columns()
626                    .get(input_ref.index)
627                    .unwrap()
628                    .name()
629                    .to_owned(),
630                _ => unreachable!(),
631            }
632        }))
633        .collect_vec();
634
635    Ok(PlanRoot::new_with_logical_plan(
636        logical_project,
637        required_dist,
638        Order::new(
639            index_columns
640                .iter()
641                .enumerate()
642                .map(|(i, (_, order))| ColumnOrder::new(i, *order))
643                .collect(),
644        ),
645        project_required_cols,
646        out_names,
647    ))
648}
649
650/// Note: distributed by columns must be a prefix of index columns, so we just use
651/// `distributed_by_columns_len` to represent distributed by columns
652fn assemble_materialize(
653    database_id: DatabaseId,
654    schema_id: SchemaId,
655    table_catalog: Arc<TableCatalog>,
656    context: OptimizerContextRef,
657    index_name: String,
658    index_columns: &[(ExprImpl, OrderType)],
659    include_columns: &[ExprImpl],
660    distributed_by_columns_len: usize,
661) -> Result<StreamMaterialize> {
662    let input = assemble_input(
663        table_catalog.clone(),
664        context.clone(),
665        index_columns,
666        include_columns,
667        // schema of logical_project is such that index columns come first.
668        // so we can use distributed_by_columns_len to represent distributed by columns indices.
669        RequiredDist::PhysicalDist(Distribution::HashShard(
670            (0..distributed_by_columns_len).collect(),
671        )),
672    )?;
673
674    let definition = context.normalized_sql().to_owned();
675    let retention_seconds = table_catalog.retention_seconds.and_then(NonZeroU32::new);
676
677    input.gen_index_plan(
678        index_name,
679        database_id,
680        schema_id,
681        definition,
682        retention_seconds,
683    )
684}
685
686pub async fn handle_create_index(
687    mut handler_args: HandlerArgs,
688    if_not_exists: bool,
689    index_name: ObjectName,
690    table_name: ObjectName,
691    method: Option<Ident>,
692    columns: Vec<OrderByExpr>,
693    include: Vec<Ident>,
694    distributed_by: Vec<ast::Expr>,
695) -> Result<RwPgResponse> {
696    let session = handler_args.session.clone();
697    let resource_type =
698        resolve_streaming_job_resource_type(session.as_ref(), &mut handler_args.with_options)?;
699
700    let (graph, index_table, index) = {
701        let (schema_name, table, index_table_name) =
702            resolve_index_schema(&session, index_name, table_name)?;
703        let qualified_index_name = ObjectName(vec![
704            Ident::from_real_value(&schema_name),
705            Ident::from_real_value(&index_table_name),
706        ]);
707        if let Either::Right(resp) = session.check_relation_name_duplicated(
708            qualified_index_name,
709            StatementType::CREATE_INDEX,
710            if_not_exists,
711        )? {
712            return Ok(resp);
713        }
714
715        let context = OptimizerContext::from_handler_args(handler_args);
716        let (plan, index_table, index) = gen_create_index_plan(
717            &session,
718            context.into(),
719            schema_name,
720            table,
721            index_table_name,
722            method,
723            columns,
724            include,
725            distributed_by,
726        )?;
727        let graph = build_graph(plan, Some(GraphJobType::Index))?;
728
729        (graph, index_table, index)
730    };
731
732    tracing::trace!(
733        "name={}, graph=\n{}",
734        index.name,
735        serde_json::to_string_pretty(&graph).unwrap()
736    );
737
738    let _job_guard =
739        session
740            .env()
741            .creating_streaming_job_tracker()
742            .guard(CreatingStreamingJobInfo::new(
743                session.session_id(),
744                index.database_id,
745                index.schema_id,
746                index.name.clone(),
747            ));
748
749    let catalog_writer = session.catalog_writer()?;
750    execute_with_long_running_notification(
751        catalog_writer.create_index(
752            index,
753            index_table.to_prost(),
754            graph,
755            resource_type,
756            if_not_exists,
757        ),
758        &session,
759        "CREATE INDEX",
760        LongRunningNotificationAction::MonitorBackfillJob,
761    )
762    .await?;
763
764    Ok(PgResponse::empty_result(StatementType::CREATE_INDEX))
765}