use std::collections::BTreeMap;
use pg_query::protobuf::{
AlterOwnerStmt, AlterTableSpaceOptionsStmt, CreateTableSpaceStmt, DefElem, Node, RoleSpec,
};
use crate::identifier::Identifier;
use crate::ir::cluster::catalog::ClusterCatalog;
use crate::ir::cluster::tablespace::{Tablespace, normalize_location};
use crate::parse::error::{ParseError, SourceLocation};
pub(super) fn apply_create(
s: &CreateTableSpaceStmt,
cat: &mut ClusterCatalog,
loc: &SourceLocation,
) -> Result<(), ParseError> {
let name = parse_name(&s.tablespacename, loc)?;
if cat.tablespaces.iter().any(|t| t.name == name) {
return Err(ParseError::Structural {
location: loc.clone(),
message: format!("tablespace {name} declared more than once"),
});
}
if s.location.is_empty() {
return Err(ParseError::Structural {
location: loc.clone(),
message: format!("CREATE TABLESPACE {name} requires a non-empty LOCATION"),
});
}
let owner = role_spec_to_owner(s.owner.as_ref(), loc)?;
let options = def_elems_to_map(&s.options, loc)?;
let location = normalize_location(&s.location);
cat.tablespaces.push(Tablespace {
name,
location,
owner,
options,
comment: None,
});
Ok(())
}
pub(super) fn apply_set(
s: &AlterTableSpaceOptionsStmt,
cat: &mut ClusterCatalog,
loc: &SourceLocation,
) -> Result<(), ParseError> {
let name = parse_name(&s.tablespacename, loc)?;
let ts = find_mut(cat, &name, loc, "ALTER TABLESPACE")?;
if s.is_reset {
for node in &s.options {
if let Some(pg_query::NodeEnum::DefElem(def)) = node.node.as_ref() {
ts.options.remove(&def.defname);
}
}
} else {
let merged = def_elems_to_map(&s.options, loc)?;
ts.options.extend(merged);
}
Ok(())
}
pub(super) fn apply_owner(
s: &AlterOwnerStmt,
cat: &mut ClusterCatalog,
loc: &SourceLocation,
) -> Result<(), ParseError> {
let name = match s.object.as_deref().and_then(|n| n.node.as_ref()) {
Some(pg_query::NodeEnum::String(str_node)) => parse_name(&str_node.sval, loc)?,
other => {
return Err(ParseError::Structural {
location: loc.clone(),
message: format!("ALTER TABLESPACE OWNER TO has unexpected target {other:?}"),
});
}
};
let owner =
role_spec_to_owner(s.newowner.as_ref(), loc)?.ok_or_else(|| ParseError::Structural {
location: loc.clone(),
message: format!("ALTER TABLESPACE {name} OWNER TO missing new owner"),
})?;
let ts = find_mut(cat, &name, loc, "ALTER TABLESPACE")?;
ts.owner = Some(owner);
Ok(())
}
pub(super) fn apply_comment(
cat: &mut ClusterCatalog,
name: &str,
comment: Option<String>,
loc: &SourceLocation,
) -> Result<(), ParseError> {
let name = parse_name(name, loc)?;
let ts = find_mut(cat, &name, loc, "COMMENT ON TABLESPACE")?;
ts.comment = comment;
Ok(())
}
fn parse_name(raw: &str, loc: &SourceLocation) -> Result<Identifier, ParseError> {
Identifier::from_unquoted(raw).map_err(|e| ParseError::Structural {
location: loc.clone(),
message: format!("invalid tablespace name {raw:?}: {e}"),
})
}
fn find_mut<'a>(
cat: &'a mut ClusterCatalog,
name: &Identifier,
loc: &SourceLocation,
stmt: &str,
) -> Result<&'a mut Tablespace, ParseError> {
cat.tablespaces
.iter_mut()
.find(|t| &t.name == name)
.ok_or_else(|| ParseError::Structural {
location: loc.clone(),
message: format!(
"{stmt} references unknown tablespace {name} — declare with CREATE TABLESPACE first"
),
})
}
fn role_spec_to_owner(
spec: Option<&RoleSpec>,
loc: &SourceLocation,
) -> Result<Option<Identifier>, ParseError> {
let Some(rs) = spec else {
return Ok(None);
};
let id = Identifier::from_unquoted(&rs.rolename).map_err(|e| ParseError::Structural {
location: loc.clone(),
message: format!("invalid owner role name {:?}: {e}", rs.rolename),
})?;
Ok(Some(id))
}
fn def_elems_to_map(
options: &[Node],
loc: &SourceLocation,
) -> Result<BTreeMap<String, String>, ParseError> {
let mut out = BTreeMap::new();
for node in options {
let Some(pg_query::NodeEnum::DefElem(def)) = node.node.as_ref() else {
continue;
};
let value = def_elem_value(def, loc)?;
out.insert(def.defname.clone(), value);
}
Ok(out)
}
fn def_elem_value(def: &DefElem, loc: &SourceLocation) -> Result<String, ParseError> {
let Some(arg) = def.arg.as_ref().and_then(|a| a.node.as_ref()) else {
return Err(ParseError::Structural {
location: loc.clone(),
message: format!("tablespace option '{}' missing argument", def.defname),
});
};
match arg {
pg_query::NodeEnum::Integer(i) => Ok(i.ival.to_string()),
pg_query::NodeEnum::Float(f) => Ok(f.fval.clone()),
pg_query::NodeEnum::String(s) => Ok(s.sval.clone()),
pg_query::NodeEnum::Boolean(b) => Ok(b.boolval.to_string()),
other => Err(ParseError::Structural {
location: loc.clone(),
message: format!(
"tablespace option '{}' has unsupported value {other:?}",
def.defname
),
}),
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use crate::ir::cluster::catalog::ClusterCatalog;
use crate::parse::error::ParseError;
use super::super::apply_file;
fn parse(sql: &str) -> Result<ClusterCatalog, ParseError> {
let mut cat = ClusterCatalog::empty();
apply_file(sql, Path::new("tablespaces/test.sql"), &mut cat)?;
Ok(cat)
}
#[test]
fn create_simple() {
let cat = parse("CREATE TABLESPACE t LOCATION '/x';").unwrap();
assert_eq!(cat.tablespaces.len(), 1);
let ts = &cat.tablespaces[0];
assert_eq!(ts.name.as_str(), "t");
assert_eq!(ts.location, "/x");
assert!(ts.owner.is_none());
assert!(ts.options.is_empty());
assert!(ts.comment.is_none());
}
#[test]
fn create_with_owner_and_options() {
let cat =
parse("CREATE TABLESPACE t OWNER app_owner LOCATION '/x' WITH (seq_page_cost = 2.0);")
.unwrap();
let ts = &cat.tablespaces[0];
assert_eq!(ts.owner.as_ref().map(Identifier::as_str), Some("app_owner"));
assert_eq!(
ts.options.get("seq_page_cost").map(String::as_str),
Some("2.0")
);
}
#[test]
fn alter_set_merges_option() {
let cat = parse(
"CREATE TABLESPACE t LOCATION '/x';\
ALTER TABLESPACE t SET (random_page_cost = 1.5);",
)
.unwrap();
let ts = &cat.tablespaces[0];
assert_eq!(
ts.options.get("random_page_cost").map(String::as_str),
Some("1.5")
);
}
#[test]
fn alter_reset_removes_option() {
let cat = parse(
"CREATE TABLESPACE t LOCATION '/x' WITH (seq_page_cost = 2.0);\
ALTER TABLESPACE t RESET (seq_page_cost);",
)
.unwrap();
assert!(cat.tablespaces[0].options.is_empty());
}
#[test]
fn alter_owner_to() {
let cat = parse(
"CREATE TABLESPACE t LOCATION '/x';\
ALTER TABLESPACE t OWNER TO app_owner;",
)
.unwrap();
assert_eq!(
cat.tablespaces[0].owner.as_ref().map(Identifier::as_str),
Some("app_owner")
);
}
#[test]
fn comment_on_tablespace() {
let cat = parse(
"CREATE TABLESPACE t LOCATION '/x';\
COMMENT ON TABLESPACE t IS 'fast storage';",
)
.unwrap();
assert_eq!(cat.tablespaces[0].comment.as_deref(), Some("fast storage"));
}
#[test]
fn duplicate_name_errors() {
let err = parse(
"CREATE TABLESPACE t LOCATION '/x';\
CREATE TABLESPACE t LOCATION '/y';",
)
.unwrap_err();
assert!(
matches!(err, ParseError::Structural { message, .. } if message.contains("more than once"))
);
}
#[test]
fn alter_before_create_errors() {
let err = parse("ALTER TABLESPACE t SET (seq_page_cost = 2.0);").unwrap_err();
assert!(
matches!(err, ParseError::Structural { message, .. } if message.contains("unknown tablespace"))
);
}
#[test]
fn drop_tablespace_rejected() {
let err = parse("DROP TABLESPACE t;").unwrap_err();
assert!(
matches!(err, ParseError::Structural { message, .. } if message.contains("DROP TABLESPACE"))
);
}
#[test]
fn rename_tablespace_rejected() {
let err = parse("ALTER TABLESPACE t RENAME TO u;").unwrap_err();
assert!(
matches!(err, ParseError::Structural { message, .. } if message.contains("RENAME"))
);
}
use crate::identifier::Identifier;
}