feat(pg,kingbase): partition table operations and detail queries

This commit is contained in:
amwps290 authored and GitHub committed 2026-09-21 15:36:49 +08:00
1 parent 882bd130ab
commit 1000aa07b6
62 files changed
+6180 -133

No files matched your search

@@ -8,6 +8,7 @@ mod foreign_keys;
mod indexes;
mod mysql_engine;
mod owner;
mod partitions;
mod triggers;
mod types;
mod util;
@@ -19,6 +20,7 @@ mod tests;
pub use column_alter::build_single_column_alter_sql;
pub use create_table::build_create_table_sql;
pub use owner::build_table_owner_change_sql;
pub use partitions::{build_create_partitioned_table_sql, build_table_partition_operation_sql};
pub use types::*;
pub use column_alter::{
@@ -23,7 +23,20 @@ fn is_sqlite_integer_family_type(data_type: &str) -> bool {
.any(|candidate| normalized == *candidate || normalized.starts_with(&format!("{candidate}(")))
}
pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStructureSqlResult {
pub fn build_create_table_sql(options: TableStructureSqlOptions) -> TableStructureSqlResult {
build_create_table_sql_with_partition_clause(options, None)
}
/// Shared implementation for plain and partitioned `CREATE TABLE`.
///
/// `partition_clause` is a ready-made `PARTITION BY ...` body (no leading
/// keyword), appended to the `CREATE TABLE` statement. Keeping it as a separate
/// entry point means `build_create_table_sql`'s signature — used from ~160 call
/// sites and integration tests — stays unchanged.
pub(super) fn build_create_table_sql_with_partition_clause(
mut options: TableStructureSqlOptions,
partition_clause: Option<String>,
) -> TableStructureSqlResult {
let capabilities;
let dialect;
if options.is_gaussdb_m_mode {
@@ -177,7 +190,12 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
column_definitions.push(format!("PRIMARY KEY ({pk_list})"));
}
statements.push(format!("CREATE TABLE {table} (\n {}\n);", column_definitions.join(",\n ")));
let create_table = format!("CREATE TABLE {table} (\n {}\n)", column_definitions.join(",\n "));
let create_table = match partition_clause {
Some(clause) => format!("{create_table} {clause}"),
None => create_table,
};
statements.push(format!("{create_table};"));
if let Some(engine) = options.mysql_engine.as_deref().map(str::trim).filter(|engine| !engine.is_empty()) {
if let Some(statement) = statements.last_mut() {
@@ -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))
}
}
@@ -1,5 +1,6 @@
use super::*;
use crate::models::connection::DatabaseType;
use crate::types::PgPartitionKind;
fn column(name: &str) -> EditableStructureColumn {
EditableStructureColumn {
@@ -8539,3 +8540,389 @@ fn postgres_expression_index_opclass_round_trips_from_indclass() {
// The bare expression text no longer carries the opclass, so it cannot be duplicated.
assert!(!sql.contains("gin_trgm_ops gin_trgm_ops"), "Opclass must not be duplicated, got: {sql}");
}
fn partition_options(
database_type: DatabaseType,
schema: Option<&str>,
table_name: &str,
operations: Vec<TablePartitionOperation>,
) -> TablePartitionSqlOptions {
TablePartitionSqlOptions {
database_type: Some(database_type),
driver_profile: None,
schema: schema.map(str::to_string),
table_name: table_name.to_string(),
operations,
}
}
fn partition_operation(
kind: TablePartitionOperationKind,
name: &str,
bound: Option<TablePartitionBoundDraft>,
) -> TablePartitionOperation {
TablePartitionOperation {
id: format!("op:{name}"),
kind,
parent_schema: String::new(),
parent_table: String::new(),
schema: String::new(),
name: name.to_string(),
bound,
concurrently: false,
}
}
#[test]
fn partition_create_uses_partition_of_with_each_bound_kind() {
let range = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"sales",
vec![partition_operation(
TablePartitionOperationKind::Create,
"sales_2025",
Some(TablePartitionBoundDraft::Range {
from: vec!["'2025-01-01'".to_string()],
to: vec!["'2026-01-01'".to_string()],
}),
)],
));
assert!(range.warnings.is_empty(), "{:?}", range.warnings);
assert_eq!(
range.statements,
vec!["CREATE TABLE \"public\".\"sales_2025\" PARTITION OF \"public\".\"sales\" FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');"]
);
let list = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"events",
vec![partition_operation(
TablePartitionOperationKind::Create,
"events_ab",
Some(TablePartitionBoundDraft::List { values: vec!["'a'".to_string(), "'b'".to_string()] }),
)],
));
assert_eq!(
list.statements,
vec!["CREATE TABLE \"public\".\"events_ab\" PARTITION OF \"public\".\"events\" FOR VALUES IN ('a', 'b');"]
);
let hash = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"h",
vec![partition_operation(
TablePartitionOperationKind::Create,
"h0",
Some(TablePartitionBoundDraft::Hash { modulus: 2, remainder: 0 }),
)],
));
assert_eq!(
hash.statements,
vec!["CREATE TABLE \"public\".\"h0\" PARTITION OF \"public\".\"h\" FOR VALUES WITH (MODULUS 2, REMAINDER 0);"]
);
let default = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"sales",
vec![partition_operation(
TablePartitionOperationKind::Create,
"sales_default",
Some(TablePartitionBoundDraft::Default),
)],
));
assert_eq!(
default.statements,
vec!["CREATE TABLE \"public\".\"sales_default\" PARTITION OF \"public\".\"sales\" DEFAULT;"]
);
}
#[test]
fn partition_attach_detach_and_drop_generate_explicit_statements() {
let attach = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"sales",
vec![partition_operation(
TablePartitionOperationKind::Attach,
"sales_2025",
Some(TablePartitionBoundDraft::Range {
from: vec!["MINVALUE".to_string()],
to: vec!["'2025-01-01'".to_string()],
}),
)],
));
assert_eq!(
attach.statements,
vec!["ALTER TABLE \"public\".\"sales\" ATTACH PARTITION \"public\".\"sales_2025\" FOR VALUES FROM (MINVALUE) TO ('2025-01-01');"]
);
let detach = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"sales",
vec![partition_operation(TablePartitionOperationKind::Detach, "sales_2024", None)],
));
assert_eq!(detach.statements, vec!["ALTER TABLE \"public\".\"sales\" DETACH PARTITION \"public\".\"sales_2024\";"]);
let mut concurrent = partition_operation(TablePartitionOperationKind::Detach, "sales_2024", None);
concurrent.concurrently = true;
let detach_concurrently = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"sales",
vec![concurrent],
));
assert_eq!(
detach_concurrently.statements,
vec!["ALTER TABLE \"public\".\"sales\" DETACH PARTITION \"public\".\"sales_2024\" CONCURRENTLY;"]
);
let drop = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"sales",
vec![partition_operation(TablePartitionOperationKind::Drop, "sales_2024", None)],
));
assert_eq!(drop.statements, vec!["DROP TABLE \"public\".\"sales_2024\";"]);
}
#[test]
fn partition_operation_can_target_an_explicit_parent_and_schema() {
// Detaching the partition currently being edited: the parent is not the
// edited table, and the child lives in another schema.
let mut detach = partition_operation(TablePartitionOperationKind::Detach, "sales_2024", None);
detach.parent_schema = "analytics".to_string();
detach.parent_table = "sales".to_string();
detach.schema = "archive".to_string();
let result = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"sales_2024",
vec![detach],
));
assert_eq!(
result.statements,
vec!["ALTER TABLE \"analytics\".\"sales\" DETACH PARTITION \"archive\".\"sales_2024\";"]
);
}
#[test]
fn partition_operations_are_refused_for_non_postgres() {
let result = build_table_partition_operation_sql(partition_options(
DatabaseType::Mysql,
Some("db"),
"sales",
vec![partition_operation(
TablePartitionOperationKind::Create,
"sales_2025",
Some(TablePartitionBoundDraft::Default),
)],
));
assert!(result.statements.is_empty());
assert_eq!(result.warnings, vec!["Partition operations are not supported for this database engine.".to_string()]);
}
#[test]
fn partition_operations_are_supported_for_kingbase() {
let result = build_table_partition_operation_sql(partition_options(
DatabaseType::Kingbase,
Some("public"),
"sales",
vec![partition_operation(
TablePartitionOperationKind::Create,
"sales_2025",
Some(TablePartitionBoundDraft::Default),
)],
));
assert!(result.warnings.is_empty(), "{:?}", result.warnings);
assert_eq!(
result.statements,
vec!["CREATE TABLE \"public\".\"sales_2025\" PARTITION OF \"public\".\"sales\" DEFAULT;"]
);
}
#[test]
fn partition_operations_warn_on_malformed_bounds() {
let mismatched = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"sales",
vec![partition_operation(
TablePartitionOperationKind::Create,
"sales_x",
Some(TablePartitionBoundDraft::Range {
from: vec!["1".to_string()],
to: vec!["2".to_string(), "3".to_string()],
}),
)],
));
assert!(mismatched.statements.is_empty());
assert!(mismatched.warnings[0].contains("RANGE partition bound"));
let bad_hash = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"h",
vec![partition_operation(
TablePartitionOperationKind::Create,
"h9",
Some(TablePartitionBoundDraft::Hash { modulus: 2, remainder: 2 }),
)],
));
assert!(bad_hash.statements.is_empty());
assert!(bad_hash.warnings[0].contains("HASH partition bound"));
let detach_with_bound = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("public"),
"sales",
vec![partition_operation(
TablePartitionOperationKind::Detach,
"sales_2024",
Some(TablePartitionBoundDraft::Default),
)],
));
assert!(detach_with_bound.statements.is_empty());
assert!(detach_with_bound.warnings[0].contains("must not carry a bound"));
}
#[test]
fn partition_operation_quotes_identifiers() {
let result = build_table_partition_operation_sql(partition_options(
DatabaseType::Postgres,
Some("we\"ird"),
"sales",
vec![partition_operation(
TablePartitionOperationKind::Create,
"part\"1",
Some(TablePartitionBoundDraft::Default),
)],
));
assert_eq!(
result.statements,
vec!["CREATE TABLE \"we\"\"ird\".\"part\"\"1\" PARTITION OF \"we\"\"ird\".\"sales\" DEFAULT;"]
);
}
#[test]
fn create_partitioned_table_appends_partition_by_clause() {
let column = EditableStructureColumn {
id: "sold_on".to_string(),
name: "sold_on".to_string(),
data_type: "date".to_string(),
is_nullable: false,
default_value: String::new(),
comment: String::new(),
is_primary_key: false,
extra: None,
original: None,
original_position: None,
marked_for_drop: false,
character_set: String::new(),
collation: String::new(),
};
let options = structure_change_options(DatabaseType::Postgres, Some("public"), "sales", vec![column]);
let result = build_create_partitioned_table_sql(
options,
TablePartitionDefinition {
kind: PgPartitionKind::Range,
columns: vec!["sold_on".to_string()],
expression: String::new(),
},
);
assert!(result.warnings.is_empty(), "{:?}", result.warnings);
assert_eq!(result.statements.len(), 1);
assert!(
result.statements[0].ends_with(") PARTITION BY RANGE (\"sold_on\");"),
"unexpected statement: {}",
result.statements[0]
);
}
#[test]
fn create_partitioned_table_uses_expression_and_multi_column_keys() {
let options = structure_change_options(
DatabaseType::Postgres,
Some("public"),
"events",
vec![column("region"), column("occurred_at")],
);
let expression = build_create_partitioned_table_sql(
options,
TablePartitionDefinition {
kind: PgPartitionKind::Hash,
columns: Vec::new(),
expression: "abs(id)".to_string(),
},
);
assert!(expression.statements[0].ends_with(") PARTITION BY HASH (abs(id));"), "{}", expression.statements[0]);
let options = structure_change_options(
DatabaseType::Postgres,
Some("public"),
"events",
vec![column("region"), column("occurred_at")],
);
let multi = build_create_partitioned_table_sql(
options,
TablePartitionDefinition {
kind: PgPartitionKind::List,
columns: vec!["region".to_string(), "occurred_at".to_string()],
expression: String::new(),
},
);
assert!(
multi.statements[0].ends_with(") PARTITION BY LIST (\"region\", \"occurred_at\");"),
"{}",
multi.statements[0]
);
}
#[test]
fn create_partitioned_table_requires_a_key_and_postgres() {
let options = structure_change_options(DatabaseType::Postgres, Some("public"), "events", vec![column("id")]);
let missing_key = build_create_partitioned_table_sql(
options,
TablePartitionDefinition { kind: PgPartitionKind::Range, columns: Vec::new(), expression: String::new() },
);
assert!(missing_key.statements.is_empty());
assert!(missing_key.warnings[0].contains("at least one column"));
let options = structure_change_options(DatabaseType::Mysql, Some("db"), "events", vec![column("id")]);
let mysql = build_create_partitioned_table_sql(
options,
TablePartitionDefinition {
kind: PgPartitionKind::Range,
columns: vec!["id".to_string()],
expression: String::new(),
},
);
assert!(mysql.statements.is_empty());
assert_eq!(mysql.warnings, vec!["Partitioning is not supported for this database engine.".to_string()]);
}
#[test]
fn create_partitioned_table_is_supported_for_kingbase() {
let options = structure_change_options(DatabaseType::Kingbase, Some("public"), "events", vec![column("region")]);
let result = build_create_partitioned_table_sql(
options,
TablePartitionDefinition {
kind: PgPartitionKind::List,
columns: vec!["region".to_string()],
expression: String::new(),
},
);
assert!(result.warnings.is_empty(), "{:?}", result.warnings);
assert!(result.statements[0].ends_with(") PARTITION BY LIST (\"region\");"), "{}", result.statements[0]);
}
@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use crate::models::connection::DatabaseType;
use crate::types::PgPartitionKind;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -258,6 +259,90 @@ pub struct TableStructureSqlOptions {
pub is_gaussdb_m_mode: bool,
}
/// Options for `build_table_partition_operation_sql`.
///
/// Partition maintenance is deliberately kept out of `TableStructureSqlOptions`:
/// the operations are explicit user actions (never diffed against introspected
/// state), and keeping them separate means a column/index draft cannot
/// accidentally trigger or suppress partition DDL.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TablePartitionSqlOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub database_type: Option<DatabaseType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub driver_profile: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
/// The edited table; also the default parent of every operation.
pub table_name: String,
#[serde(default)]
pub operations: Vec<TablePartitionOperation>,
}
/// Declarative partitioning for a table being created (`CREATE TABLE ...
/// PARTITION BY ...`). PostgreSQL only.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TablePartitionDefinition {
pub kind: PgPartitionKind,
/// Partition-key columns, used when `expression` is empty.
#[serde(default)]
pub columns: Vec<String>,
/// Raw partition-key expression (e.g. `date_trunc('month', ts)`), used
/// instead of `columns` when non-empty.
#[serde(default)]
pub expression: String,
}
/// One partition maintenance operation. The parent is the edited table unless
/// `parent_table` names another relation (used when detaching the partition
/// currently being edited).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TablePartitionOperation {
#[serde(default)]
pub id: String,
pub kind: TablePartitionOperationKind,
/// Partitioned parent's schema; empty falls back to the edited table's schema.
#[serde(default)]
pub parent_schema: String,
/// Partitioned parent; empty falls back to the edited table.
#[serde(default)]
pub parent_table: String,
/// Schema of the partition relation; empty falls back to the parent's schema.
#[serde(default)]
pub schema: String,
pub name: String,
/// Required for `create`/`attach`; must be absent for `detach`/`drop`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bound: Option<TablePartitionBoundDraft>,
/// Emit `DETACH PARTITION ... CONCURRENTLY` (PostgreSQL 14+).
#[serde(default)]
pub concurrently: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TablePartitionOperationKind {
Create,
Attach,
Detach,
Drop,
}
/// Partition bound as entered in the UI. Values are SQL literal text; the
/// builder only wraps them in the PostgreSQL `FOR VALUES` grammar, so `1`,
/// `'2024-01-01'`, `MINVALUE`, and `MAXVALUE` all round-trip unchanged.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum TablePartitionBoundDraft {
Range { from: Vec<String>, to: Vec<String> },
List { values: Vec<String> },
Hash { modulus: i32, remainder: i32 },
Default,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TableStructureSqlResult {