feat(pg,kingbase): partition table operations and detail queries
This commit is contained in:
1 parent
882bd130ab
commit
1000aa07b6
62 files changed
+6180
-133
No files matched your search
@@ -0,0 +1,207 @@
|
||||
use super::create_table::build_create_table_sql_with_partition_clause;
|
||||
use super::dialect::{capabilities_for, StructureDialect};
|
||||
use super::types::{
|
||||
TablePartitionBoundDraft, TablePartitionDefinition, TablePartitionOperation, TablePartitionOperationKind,
|
||||
TablePartitionSqlOptions, TableStructureSqlOptions, TableStructureSqlResult,
|
||||
};
|
||||
use super::util::quote_ident;
|
||||
use crate::models::connection::DatabaseType;
|
||||
use crate::types::PgPartitionKind;
|
||||
|
||||
/// Engines whose declarative-partition DDL has been verified. Only PostgreSQL
|
||||
/// and KingbaseES expose the same `pg_partitioned_table` catalog and
|
||||
/// `CREATE TABLE ... PARTITION OF` / `ATTACH` / `DETACH` syntax. openGauss-based
|
||||
/// engines (openGauss, Vastbase, GaussDB) use a different `pg_partition`
|
||||
/// catalog and are intentionally excluded until validated.
|
||||
fn supports_partition_ddl(database_type: Option<DatabaseType>) -> bool {
|
||||
matches!(database_type, Some(DatabaseType::Postgres | DatabaseType::Kingbase))
|
||||
}
|
||||
|
||||
/// Builds `CREATE TABLE ... PARTITION BY ...` for a table being created.
|
||||
///
|
||||
/// Kept separate from `build_create_table_sql` so the widely-used plain builder
|
||||
/// keeps its signature; shares its implementation through
|
||||
/// `build_create_table_sql_with_partition_clause`.
|
||||
pub fn build_create_partitioned_table_sql(
|
||||
options: TableStructureSqlOptions,
|
||||
definition: TablePartitionDefinition,
|
||||
) -> TableStructureSqlResult {
|
||||
let dialect = capabilities_for(options.database_type, options.driver_profile.as_deref()).dialect;
|
||||
if dialect != StructureDialect::Postgres || !supports_partition_ddl(options.database_type) {
|
||||
return TableStructureSqlResult {
|
||||
statements: Vec::new(),
|
||||
warnings: vec!["Partitioning is not supported for this database engine.".to_string()],
|
||||
};
|
||||
}
|
||||
let mut warnings = Vec::new();
|
||||
let Some(clause) = format_partition_clause(&definition, &mut warnings) else {
|
||||
return TableStructureSqlResult { statements: Vec::new(), warnings };
|
||||
};
|
||||
build_create_table_sql_with_partition_clause(options, Some(clause))
|
||||
}
|
||||
|
||||
/// Renders the body of a `PARTITION BY` clause (without the leading keyword),
|
||||
/// e.g. `RANGE (region, sold_on)`.
|
||||
fn format_partition_clause(definition: &TablePartitionDefinition, warnings: &mut Vec<String>) -> Option<String> {
|
||||
let keyword = match definition.kind {
|
||||
PgPartitionKind::Range => "RANGE",
|
||||
PgPartitionKind::List => "LIST",
|
||||
PgPartitionKind::Hash => "HASH",
|
||||
};
|
||||
let expression = definition.expression.trim();
|
||||
let key = if !expression.is_empty() {
|
||||
expression.to_string()
|
||||
} else {
|
||||
let columns = definition
|
||||
.columns
|
||||
.iter()
|
||||
.map(|column| column.trim())
|
||||
.filter(|column| !column.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if columns.is_empty() {
|
||||
warnings.push("A partition key needs at least one column or an expression.".to_string());
|
||||
return None;
|
||||
}
|
||||
columns.iter().map(|column| quote_ident(StructureDialect::Postgres, column)).collect::<Vec<_>>().join(", ")
|
||||
};
|
||||
Some(format!("PARTITION BY {keyword} ({key})"))
|
||||
}
|
||||
|
||||
/// Builds the DDL for explicit partition maintenance operations
|
||||
/// (create / attach / detach / drop).
|
||||
///
|
||||
/// Gated to the engines in [`supports_partition_ddl`]; a refusal empties the
|
||||
/// statement list and pushes one warning. Callers surface the warning and block
|
||||
/// the save, so an unsupported request never silently degrades.
|
||||
pub fn build_table_partition_operation_sql(options: TablePartitionSqlOptions) -> TableStructureSqlResult {
|
||||
if options.operations.is_empty() {
|
||||
return TableStructureSqlResult { statements: Vec::new(), warnings: Vec::new() };
|
||||
}
|
||||
let dialect = capabilities_for(options.database_type, options.driver_profile.as_deref()).dialect;
|
||||
if dialect != StructureDialect::Postgres || !supports_partition_ddl(options.database_type) {
|
||||
return TableStructureSqlResult {
|
||||
statements: Vec::new(),
|
||||
warnings: vec!["Partition operations are not supported for this database engine.".to_string()],
|
||||
};
|
||||
}
|
||||
|
||||
let default_schema = options.schema.as_deref().unwrap_or("");
|
||||
let mut warnings = Vec::new();
|
||||
let mut statements = Vec::new();
|
||||
for operation in &options.operations {
|
||||
if let Some(statement) = build_operation_sql(operation, default_schema, &options.table_name, &mut warnings) {
|
||||
statements.push(statement);
|
||||
}
|
||||
}
|
||||
TableStructureSqlResult { statements, warnings }
|
||||
}
|
||||
|
||||
fn build_operation_sql(
|
||||
operation: &TablePartitionOperation,
|
||||
default_schema: &str,
|
||||
default_parent_table: &str,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> Option<String> {
|
||||
let dialect = StructureDialect::Postgres;
|
||||
let parent_schema = pick(&operation.parent_schema, default_schema);
|
||||
let parent_table = pick(&operation.parent_table, default_parent_table);
|
||||
let child_schema = pick(&operation.schema, &parent_schema);
|
||||
let name = operation.name.trim();
|
||||
if parent_table.is_empty() {
|
||||
warnings.push("A partition operation requires a parent table.".to_string());
|
||||
return None;
|
||||
}
|
||||
if name.is_empty() {
|
||||
warnings.push("A partition operation requires a partition name.".to_string());
|
||||
return None;
|
||||
}
|
||||
|
||||
let parent = qualify(dialect, &parent_schema, &parent_table);
|
||||
let child = qualify(dialect, &child_schema, name);
|
||||
match operation.kind {
|
||||
TablePartitionOperationKind::Create => {
|
||||
let bound = require_bound(operation, "create", warnings)?;
|
||||
Some(format!("CREATE TABLE {child} PARTITION OF {parent} {bound};"))
|
||||
}
|
||||
TablePartitionOperationKind::Attach => {
|
||||
let bound = require_bound(operation, "attach", warnings)?;
|
||||
Some(format!("ALTER TABLE {parent} ATTACH PARTITION {child} {bound};"))
|
||||
}
|
||||
TablePartitionOperationKind::Detach => {
|
||||
reject_bound(operation, "detach", warnings)?;
|
||||
let concurrently = if operation.concurrently { " CONCURRENTLY" } else { "" };
|
||||
Some(format!("ALTER TABLE {parent} DETACH PARTITION {child}{concurrently};"))
|
||||
}
|
||||
TablePartitionOperationKind::Drop => {
|
||||
reject_bound(operation, "drop", warnings)?;
|
||||
Some(format!("DROP TABLE {child};"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn require_bound(operation: &TablePartitionOperation, action: &str, warnings: &mut Vec<String>) -> Option<String> {
|
||||
let Some(bound) = operation.bound.as_ref() else {
|
||||
warnings.push(format!("A partition {action} operation requires a bound definition."));
|
||||
return None;
|
||||
};
|
||||
format_bound(bound, warnings)
|
||||
}
|
||||
|
||||
fn reject_bound(operation: &TablePartitionOperation, action: &str, warnings: &mut Vec<String>) -> Option<()> {
|
||||
if operation.bound.is_some() {
|
||||
warnings.push(format!("A partition {action} operation must not carry a bound definition."));
|
||||
return None;
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Renders a bound as the `FOR VALUES ...` clause PostgreSQL expects (or a bare
|
||||
/// `DEFAULT`). Returns `None` and records a warning when the bound is
|
||||
/// malformed, so the caller can drop just that statement.
|
||||
fn format_bound(bound: &TablePartitionBoundDraft, warnings: &mut Vec<String>) -> Option<String> {
|
||||
match bound {
|
||||
TablePartitionBoundDraft::Default => Some("DEFAULT".to_string()),
|
||||
TablePartitionBoundDraft::Range { from, to } => {
|
||||
if from.is_empty() || from.len() != to.len() {
|
||||
warnings
|
||||
.push("A RANGE partition bound needs a FROM and TO tuple of equal, non-zero length.".to_string());
|
||||
return None;
|
||||
}
|
||||
let from = from.iter().map(|value| value.trim()).collect::<Vec<_>>().join(", ");
|
||||
let to = to.iter().map(|value| value.trim()).collect::<Vec<_>>().join(", ");
|
||||
Some(format!("FOR VALUES FROM ({from}) TO ({to})"))
|
||||
}
|
||||
TablePartitionBoundDraft::List { values } => {
|
||||
if values.is_empty() {
|
||||
warnings.push("A LIST partition bound needs at least one value.".to_string());
|
||||
return None;
|
||||
}
|
||||
let values = values.iter().map(|value| value.trim()).collect::<Vec<_>>().join(", ");
|
||||
Some(format!("FOR VALUES IN ({values})"))
|
||||
}
|
||||
TablePartitionBoundDraft::Hash { modulus, remainder } => {
|
||||
if *modulus <= 0 || *remainder < 0 || remainder >= modulus {
|
||||
warnings.push("A HASH partition bound needs MODULUS > 0 and 0 <= REMAINDER < MODULUS.".to_string());
|
||||
return None;
|
||||
}
|
||||
Some(format!("FOR VALUES WITH (MODULUS {modulus}, REMAINDER {remainder})"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pick(value: &str, fallback: &str) -> String {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
fallback.trim().to_string()
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn qualify(dialect: StructureDialect, schema: &str, table: &str) -> String {
|
||||
if schema.is_empty() {
|
||||
quote_ident(dialect, table)
|
||||
} else {
|
||||
format!("{}.{}", quote_ident(dialect, schema), quote_ident(dialect, table))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user