feat(postgres): support concurrent index creation
This commit is contained in:
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();
|
||||
|
||||
Reference in new issue
Block a user