1use 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 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)?, ef_construction: parse_non_zero_u32("ef_construction", 64)?, 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 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 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 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 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 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 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 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 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 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 let stored_primary_columns: HashSet<usize> = index_item
468 .iter()
469 .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 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
650fn 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 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}