feat(postgres): support concurrent index creation

This commit is contained in:
jearhe authored and GitHub committed 2026-08-17 16:31:44 +08:00
1 parent ebf412b144
commit aa6d60533c
38 files changed
+1917 -34

No files matched your search

@@ -9,12 +9,16 @@ use super::indexes::build_create_index_statements;
use super::triggers::build_trigger_sql;
use super::types::{TableStructureSqlOptions, TableStructureSqlResult};
use super::util::{clean, format_default_for_sql, normalize_default, qualified_table, quote_ident, quote_string};
use super::validation::{validate_columns, validate_dameng_identity};
use super::validation::{validate_columns, validate_concurrent_index_scope, validate_dameng_identity};
use crate::models::connection::DatabaseType;
pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStructureSqlResult {
options.table_name = clean(&options.table_name);
let mut warnings = Vec::new();
// Fail closed: a concurrent-index request on a partitioned parent (or on an
// existing index in a hand-built draft) is refused up front instead of
// degrading into blocking index DDL.
warnings.extend(validate_concurrent_index_scope(&options));
if options.table_name.is_empty() {
warnings.push("Table name is required.".to_string());
}
@@ -183,6 +187,7 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
options.schema.as_deref(),
&options.table_name,
false,
capabilities.index_concurrent,
));
}
@@ -38,6 +38,7 @@ pub(super) struct TableStructureCapabilities {
pub(super) index_include: bool,
pub(super) index_filter: bool,
pub(super) index_comment: bool,
pub(super) index_concurrent: bool,
pub(super) alter_primary_key: bool,
pub(super) foreign_key: bool,
}
@@ -59,6 +60,7 @@ impl Default for TableStructureCapabilities {
index_include: false,
index_filter: false,
index_comment: false,
index_concurrent: false,
alter_primary_key: false,
foreign_key: false,
}
@@ -118,24 +120,35 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
| DatabaseType::Vastbase
| DatabaseType::Kingbase
| DatabaseType::Firebird,
) => TableStructureCapabilities {
dialect: StructureDialect::Postgres,
add_column: true,
drop_column: true,
rename_column: true,
alter_existing_column: true,
comment: true,
create_index: true,
drop_index: true,
rebuild_index: true,
index_type: true,
index_include: true,
index_filter: true,
index_comment: true,
alter_primary_key: true,
foreign_key: true,
..base
},
) => {
let mut caps = TableStructureCapabilities {
dialect: StructureDialect::Postgres,
add_column: true,
drop_column: true,
rename_column: true,
alter_existing_column: true,
comment: true,
create_index: true,
drop_index: true,
rebuild_index: true,
index_type: true,
index_include: true,
index_filter: true,
index_comment: true,
alter_primary_key: true,
foreign_key: true,
..base
};
// CREATE INDEX CONCURRENTLY is PostgreSQL-specific. Other PostgreSQL-family
// engines (Kingbase, GaussDB, OpenGauss, HighGo, UXDB, Vastbase, KWDB,
// Firebird) share the Postgres dialect but have not been verified against a
// real environment, so they keep this capability off until individually
// validated.
if database_type == Some(DatabaseType::Postgres) {
caps.index_concurrent = true;
}
caps
}
Some(DatabaseType::Questdb) => TableStructureCapabilities {
dialect: StructureDialect::Questdb,
add_column: true,
@@ -151,6 +164,7 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
index_include: false,
index_filter: false,
index_comment: false,
index_concurrent: false,
alter_primary_key: false,
foreign_key: false,
},
@@ -66,6 +66,7 @@ pub(super) fn build_index_sql(options: &TableStructureSqlOptions, warnings: &mut
options.schema.as_deref(),
&options.table_name,
or_replace,
capabilities.index_concurrent,
));
continue;
}
@@ -82,6 +83,7 @@ pub(super) fn build_index_sql(options: &TableStructureSqlOptions, warnings: &mut
options.schema.as_deref(),
&options.table_name,
false,
capabilities.index_concurrent,
));
}
@@ -217,6 +219,7 @@ pub(super) fn build_create_index_statements(
schema: Option<&str>,
table_name: &str,
or_replace: bool,
concurrently_supported: bool,
) -> Vec<String> {
let capabilities = capabilities_for(database_type_for_dialect(dialect));
let name = clean(&index.name);
@@ -226,6 +229,14 @@ pub(super) fn build_create_index_statements(
return Vec::new();
}
// `CREATE INDEX CONCURRENTLY` is PostgreSQL-only: the flag is honored only when
// the caller's real database type advertises the capability, so a stale or
// forged `concurrently: true` from another engine is ignored and the original
// SQL is generated unchanged. Unsupported concurrent requests (existing index,
// partitioned parent) are rejected up front by `validate_concurrent_index_scope`
// before any statement is built — this function never downgrades them.
let concurrently = index.concurrently && concurrently_supported && dialect == StructureDialect::Postgres;
let unique = if index.is_unique { "UNIQUE " } else { "" };
let replace = if or_replace { "OR REPLACE " } else { "" };
let key_is_expression = key_expression_flags(index, &columns);
@@ -296,8 +307,9 @@ pub(super) fn build_create_index_statements(
let create_table =
if dialect == StructureDialect::Sqlite { quote_ident(dialect, table_name) } else { table.to_string() };
let create_sql = if dialect == StructureDialect::Postgres {
let concurrent_clause = if concurrently { "CONCURRENTLY " } else { "" };
format!(
"CREATE {replace}{unique}{type_prefix}INDEX {index_name} ON {create_table}{using_clause} ({cols}){include_clause}{where_clause};"
"CREATE {replace}{unique}INDEX {concurrent_clause}{index_name} ON {create_table}{using_clause} ({cols}){include_clause}{where_clause};"
)
} else {
format!(
@@ -1516,6 +1516,7 @@ mod tests {
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
partitioned: false,
}
}
@@ -1691,6 +1692,7 @@ mod tests {
index_type: String::new(),
included_columns: Vec::new(),
comment: String::new(),
concurrently: false,
original: None,
marked_for_drop: false,
});
@@ -2009,6 +2011,7 @@ mod tests {
index_type: String::new(),
included_columns: Vec::new(),
comment: String::new(),
concurrently: false,
original: Some(IndexInfo {
name: "uq_items_value".to_string(),
columns: vec!["value".to_string()],
File diff suppressed because it is too large. Load diff
@@ -96,6 +96,8 @@ pub struct EditableStructureIndex {
pub included_columns: Vec<String>,
#[serde(default)]
pub comment: String,
#[serde(default)]
pub concurrently: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub original: Option<IndexInfo>,
#[serde(default)]
@@ -208,6 +210,13 @@ pub struct TableStructureSqlOptions {
pub table_comment: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub original_table_comment: Option<String>,
/// Whether the target table is a partitioned parent table (PostgreSQL
/// `relkind = 'p'`). PostgreSQL rejects `CREATE INDEX CONCURRENTLY` on
/// partitioned parents, so the builder refuses such a request up front
/// (`validate_concurrent_index_scope`) instead of emitting SQL the server
/// will reject or downgrading to a blocking `CREATE INDEX`.
#[serde(default)]
pub partitioned: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -4,6 +4,40 @@ use super::indexes::has_existing_index_change;
use super::types::{EditableStructureColumn, TableStructureSqlOptions};
use super::util::clean;
/// Hard errors for `CREATE INDEX CONCURRENTLY` requests the editor cannot
/// honor safely. Unlike the advisory warnings in [`validate_draft`], these
/// reject the whole save plan: the caller asked for a non-blocking build and
/// the only way to honor that is the requested concurrent DDL, so falling back
/// to blocking index DDL must never happen silently.
///
/// Only real PostgreSQL advertises `index_concurrent`, so stale/forged
/// `concurrently` flags on other engines are ignored here (they cannot express
/// a concurrent request in the first place).
pub(super) fn validate_concurrent_index_scope(options: &TableStructureSqlOptions) -> Vec<String> {
let mut errors = Vec::new();
let capabilities = capabilities_for(options.database_type);
if !capabilities.index_concurrent || capabilities.dialect != StructureDialect::Postgres {
return errors;
}
for index in options.indexes.iter().filter(|index| !index.marked_for_drop) {
if !index.concurrently {
continue;
}
if index.original.is_some() {
errors.push(format!(
"CREATE INDEX CONCURRENTLY is only supported for newly created indexes. Editing an existing index \"{}\" with Concurrent enabled is not supported.",
index.name
));
} else if options.partitioned {
errors.push(
"CREATE INDEX CONCURRENTLY is not supported on PostgreSQL partitioned parent tables. Create indexes concurrently on individual partitions and attach them separately."
.to_string(),
);
}
}
errors
}
pub(super) fn validate_draft(options: &TableStructureSqlOptions) -> Vec<String> {
let mut warnings = Vec::new();
let active_columns: Vec<_> = options.columns.iter().filter(|column| !column.marked_for_drop).collect();