use std::collections::BTreeMap;
use pg_query::NodeEnum;
use pg_query::protobuf::{AlterSubscriptionStmt, AlterSubscriptionType, CreateSubscriptionStmt};
use crate::identifier::Identifier;
use crate::ir::subscription::{OriginMode, StreamingMode, Subscription, SubscriptionOptions};
use crate::parse::builder::shared;
use crate::parse::error::{ParseError, SourceLocation};
pub(crate) fn parse_create_subscription(
stmt: &CreateSubscriptionStmt,
source_loc: SourceLocation,
existing: &mut BTreeMap<Identifier, Subscription>,
) -> Result<(), ParseError> {
let name = Identifier::from_unquoted(&stmt.subname)
.map_err(|e| ParseError::InvalidIdentifier(stmt.subname.clone(), e.to_string()))?;
if existing.contains_key(&name) {
return Err(ParseError::DuplicateSubscription(name, source_loc));
}
if stmt.conninfo.is_empty() {
return Err(ParseError::SubscriptionEmptyConnection(name, source_loc));
}
let publications = parse_publication_list(&stmt.publication, &name, &source_loc)?;
if publications.is_empty() {
return Err(ParseError::SubscriptionEmptyPublications(name, source_loc));
}
let options = parse_subscription_options(&stmt.options, &name, source_loc)?;
existing.insert(
name.clone(),
Subscription {
name,
connection: stmt.conninfo.clone(),
publications,
options,
owner: None,
comment: None,
},
);
Ok(())
}
pub(crate) fn parse_alter_subscription(
stmt: &AlterSubscriptionStmt,
source_loc: SourceLocation,
existing: &mut BTreeMap<Identifier, Subscription>,
) -> Result<(), ParseError> {
let name = Identifier::from_unquoted(&stmt.subname)
.map_err(|e| ParseError::InvalidIdentifier(stmt.subname.clone(), e.to_string()))?;
let kind =
AlterSubscriptionType::try_from(stmt.kind).unwrap_or(AlterSubscriptionType::Undefined);
match kind {
AlterSubscriptionType::AlterSubscriptionRefresh => {
return Err(ParseError::SubscriptionRefreshNotSupported(
name, source_loc,
));
}
AlterSubscriptionType::AlterSubscriptionSkip => {
return Err(ParseError::SubscriptionSkipNotSupported(name, source_loc));
}
_ => {}
}
let sub = existing.get_mut(&name).ok_or_else(|| {
ParseError::AlterSubscriptionBeforeCreate(name.clone(), source_loc.clone())
})?;
match kind {
AlterSubscriptionType::AlterSubscriptionAddPublication => {
let new_pubs = parse_publication_list(&stmt.publication, &name, &source_loc)?;
for p in new_pubs {
if !sub.publications.contains(&p) {
sub.publications.push(p);
}
}
}
AlterSubscriptionType::AlterSubscriptionDropPublication => {
let drop_pubs = parse_publication_list(&stmt.publication, &name, &source_loc)?;
sub.publications.retain(|p| !drop_pubs.contains(p));
}
AlterSubscriptionType::AlterSubscriptionSetPublication => {
let new_pubs = parse_publication_list(&stmt.publication, &name, &source_loc)?;
sub.publications = new_pubs;
}
AlterSubscriptionType::AlterSubscriptionConnection => {
sub.connection.clone_from(&stmt.conninfo);
}
AlterSubscriptionType::AlterSubscriptionOptions => {
let delta = parse_subscription_options(&stmt.options, &name, source_loc)?;
merge_options(&mut sub.options, delta);
}
AlterSubscriptionType::AlterSubscriptionEnabled => {
return Err(ParseError::SubscriptionStandaloneEnableDisableNotSupported(
name, source_loc,
));
}
AlterSubscriptionType::AlterSubscriptionRefresh
| AlterSubscriptionType::AlterSubscriptionSkip => {
unreachable!("refresh/skip handled before existence check");
}
AlterSubscriptionType::Undefined => {
}
}
Ok(())
}
fn parse_publication_list(
nodes: &[pg_query::protobuf::Node],
name: &Identifier,
loc: &SourceLocation,
) -> Result<Vec<Identifier>, ParseError> {
nodes
.iter()
.map(|n| {
let s = shared::node_string_value(n).ok_or_else(|| {
ParseError::SubscriptionOptionMalformed(name.clone(), loc.clone())
})?;
Identifier::from_unquoted(&s)
.map_err(|e| ParseError::InvalidIdentifier(s, e.to_string()))
})
.collect()
}
fn parse_subscription_options(
options: &[pg_query::protobuf::Node],
name: &Identifier,
loc: SourceLocation,
) -> Result<SubscriptionOptions, ParseError> {
let mut opts = SubscriptionOptions::default();
for opt in options {
let Some(NodeEnum::DefElem(def)) = opt.node.as_ref() else {
return Err(ParseError::SubscriptionOptionMalformed(name.clone(), loc));
};
match def.defname.as_str() {
"enabled" => {
opts.enabled = Some(extract_def_elem_bool(def, name, &loc)?);
}
"slot_name" => {
let s = extract_def_elem_text(def, name, &loc)?;
let id = Identifier::from_unquoted(&s)
.map_err(|e| ParseError::InvalidIdentifier(s, e.to_string()))?;
opts.slot_name = Some(id);
}
"connect" => {
opts.connect = Some(extract_def_elem_bool(def, name, &loc)?);
}
"create_slot" => {
opts.create_slot = Some(extract_def_elem_bool(def, name, &loc)?);
}
"copy_data" => {
opts.copy_data = Some(extract_def_elem_bool(def, name, &loc)?);
}
"synchronous_commit" => {
opts.synchronous_commit = Some(extract_def_elem_text(def, name, &loc)?);
}
"binary" => {
opts.binary = Some(extract_def_elem_bool(def, name, &loc)?);
}
"streaming" => {
let s = extract_def_elem_text(def, name, &loc)?;
opts.streaming = Some(parse_streaming_mode(&s, name, &loc)?);
}
"two_phase" => {
opts.two_phase = Some(extract_def_elem_bool(def, name, &loc)?);
}
"disable_on_error" => {
opts.disable_on_error = Some(extract_def_elem_bool(def, name, &loc)?);
}
"password_required" => {
opts.password_required = Some(extract_def_elem_bool(def, name, &loc)?);
}
"run_as_owner" => {
opts.run_as_owner = Some(extract_def_elem_bool(def, name, &loc)?);
}
"origin" => {
let s = extract_def_elem_text(def, name, &loc)?;
opts.origin = Some(parse_origin_mode(&s, name, &loc)?);
}
"failover" => {
opts.failover = Some(extract_def_elem_bool(def, name, &loc)?);
}
other => {
return Err(ParseError::UnknownSubscriptionOption(
other.to_string(),
name.clone(),
loc,
));
}
}
}
Ok(opts)
}
fn parse_streaming_mode(
s: &str,
name: &Identifier,
loc: &SourceLocation,
) -> Result<StreamingMode, ParseError> {
match s.to_ascii_lowercase().as_str() {
"off" | "false" => Ok(StreamingMode::Off),
"on" | "true" => Ok(StreamingMode::On),
"parallel" => Ok(StreamingMode::Parallel),
_ => Err(ParseError::UnknownStreamingMode(
s.to_string(),
name.clone(),
loc.clone(),
)),
}
}
fn parse_origin_mode(
s: &str,
name: &Identifier,
loc: &SourceLocation,
) -> Result<OriginMode, ParseError> {
match s.to_ascii_lowercase().as_str() {
"any" => Ok(OriginMode::Any),
"none" => Ok(OriginMode::None),
_ => Err(ParseError::UnknownOriginMode(
s.to_string(),
name.clone(),
loc.clone(),
)),
}
}
fn merge_options(base: &mut SubscriptionOptions, delta: SubscriptionOptions) {
if delta.enabled.is_some() {
base.enabled = delta.enabled;
}
if delta.slot_name.is_some() {
base.slot_name = delta.slot_name;
}
if delta.connect.is_some() {
base.connect = delta.connect;
}
if delta.create_slot.is_some() {
base.create_slot = delta.create_slot;
}
if delta.copy_data.is_some() {
base.copy_data = delta.copy_data;
}
if delta.synchronous_commit.is_some() {
base.synchronous_commit = delta.synchronous_commit;
}
if delta.binary.is_some() {
base.binary = delta.binary;
}
if delta.streaming.is_some() {
base.streaming = delta.streaming;
}
if delta.two_phase.is_some() {
base.two_phase = delta.two_phase;
}
if delta.disable_on_error.is_some() {
base.disable_on_error = delta.disable_on_error;
}
if delta.password_required.is_some() {
base.password_required = delta.password_required;
}
if delta.run_as_owner.is_some() {
base.run_as_owner = delta.run_as_owner;
}
if delta.origin.is_some() {
base.origin = delta.origin;
}
if delta.failover.is_some() {
base.failover = delta.failover;
}
}
fn extract_def_elem_text(
def: &pg_query::protobuf::DefElem,
name: &Identifier,
loc: &SourceLocation,
) -> Result<String, ParseError> {
let arg = def
.arg
.as_ref()
.and_then(|n| n.node.as_ref())
.ok_or_else(|| ParseError::SubscriptionOptionMalformed(name.clone(), loc.clone()))?;
match arg {
NodeEnum::String(s) => Ok(s.sval.clone()),
NodeEnum::AConst(ac) => {
use pg_query::protobuf::a_const::Val;
match ac.val.as_ref() {
Some(Val::Sval(s)) => Ok(s.sval.clone()),
_ => Err(ParseError::SubscriptionOptionMalformed(
name.clone(),
loc.clone(),
)),
}
}
NodeEnum::TypeName(tn) => {
tn.names
.iter()
.rev()
.find_map(|n| match n.node.as_ref() {
Some(NodeEnum::String(s)) if !s.sval.is_empty() => Some(s.sval.clone()),
_ => None,
})
.ok_or_else(|| ParseError::SubscriptionOptionMalformed(name.clone(), loc.clone()))
}
_ => Err(ParseError::SubscriptionOptionMalformed(
name.clone(),
loc.clone(),
)),
}
}
fn extract_def_elem_bool(
def: &pg_query::protobuf::DefElem,
name: &Identifier,
loc: &SourceLocation,
) -> Result<bool, ParseError> {
let Some(arg_node) = def.arg.as_ref().and_then(|n| n.node.as_ref()) else {
return Ok(true);
};
match arg_node {
NodeEnum::Boolean(b) => Ok(b.boolval),
NodeEnum::AConst(ac) => {
use pg_query::protobuf::a_const::Val;
match ac.val.as_ref() {
Some(Val::Boolval(b)) => Ok(b.boolval),
Some(Val::Sval(s)) => parse_bool_str(&s.sval, &def.defname, name, loc),
_ => Err(ParseError::SubscriptionOptionMalformed(
name.clone(),
loc.clone(),
)),
}
}
NodeEnum::TypeName(tn) => {
let raw = tn
.names
.iter()
.rev()
.find_map(|n| match n.node.as_ref() {
Some(NodeEnum::String(s)) if !s.sval.is_empty() => Some(s.sval.clone()),
_ => None,
})
.ok_or_else(|| {
ParseError::SubscriptionOptionMalformed(name.clone(), loc.clone())
})?;
parse_bool_str(&raw, &def.defname, name, loc)
}
NodeEnum::String(s) => parse_bool_str(&s.sval, &def.defname, name, loc),
_ => Err(ParseError::SubscriptionOptionMalformed(
name.clone(),
loc.clone(),
)),
}
}
fn parse_bool_str(
v: &str,
key: &str,
name: &Identifier,
loc: &SourceLocation,
) -> Result<bool, ParseError> {
match v.to_ascii_lowercase().as_str() {
"true" | "on" | "1" | "yes" => Ok(true),
"false" | "off" | "0" | "no" => Ok(false),
_ => Err(ParseError::Structural {
location: loc.clone(),
message: format!(
"subscription {name:?}: option {key:?} = {v:?} is not a valid boolean"
),
}),
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use tempfile::tempdir;
use super::*;
use crate::ir::catalog::Catalog;
use crate::parse::parse_directory;
fn write(dir: &std::path::Path, rel: &str, contents: &str) {
let p = dir.join(rel);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(p, contents).unwrap();
}
fn parse_source(sql: &str) -> Result<Catalog, ParseError> {
let tmp = tempdir().expect("tempdir");
write(tmp.path(), "schema.sql", sql);
parse_directory(tmp.path(), &[])
}
fn loc() -> SourceLocation {
SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
}
fn parse_one_create_stmt(sql: &str) -> pg_query::protobuf::CreateSubscriptionStmt {
let parsed = pg_query::parse(sql).expect("pg_query parse");
let node = parsed
.protobuf
.stmts
.into_iter()
.next()
.and_then(|r| r.stmt)
.and_then(|n| n.node)
.expect("stmt");
let NodeEnum::CreateSubscriptionStmt(s) = node else {
panic!("expected CreateSubscriptionStmt, got something else");
};
s
}
fn parse_one_alter_stmt(sql: &str) -> pg_query::protobuf::AlterSubscriptionStmt {
let parsed = pg_query::parse(sql).expect("pg_query parse");
let node = parsed
.protobuf
.stmts
.into_iter()
.next()
.and_then(|r| r.stmt)
.and_then(|n| n.node)
.expect("stmt");
let NodeEnum::AlterSubscriptionStmt(s) = node else {
panic!("expected AlterSubscriptionStmt, got something else");
};
s
}
#[test]
fn create_minimal() {
let sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p;";
let stmt = parse_one_create_stmt(sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&stmt, loc(), &mut acc).expect("ok");
let sub = acc.values().next().unwrap();
assert_eq!(sub.name.as_str(), "s");
assert_eq!(sub.connection, "host=x");
assert_eq!(sub.publications.len(), 1);
assert_eq!(sub.publications[0].as_str(), "p");
}
#[test]
fn create_multi_publication() {
let sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p, q;";
let stmt = parse_one_create_stmt(sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&stmt, loc(), &mut acc).expect("ok");
let sub = acc.values().next().unwrap();
assert_eq!(sub.publications.len(), 2);
let names: Vec<_> = sub
.publications
.iter()
.map(crate::identifier::Identifier::as_str)
.collect();
assert!(names.contains(&"p"));
assert!(names.contains(&"q"));
}
#[test]
fn create_with_options() {
let sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p \
WITH (enabled = false, binary = true, streaming = parallel);";
let stmt = parse_one_create_stmt(sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&stmt, loc(), &mut acc).expect("ok");
let sub = acc.values().next().unwrap();
assert_eq!(sub.options.enabled, Some(false));
assert_eq!(sub.options.binary, Some(true));
assert_eq!(sub.options.streaming, Some(StreamingMode::Parallel));
}
#[test]
fn create_with_connect_false() {
let sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p \
WITH (connect = false);";
let stmt = parse_one_create_stmt(sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&stmt, loc(), &mut acc).expect("ok");
let sub = acc.values().next().unwrap();
assert_eq!(
sub.options.connect,
Some(false),
"connect = false should parse to Some(false)"
);
}
#[test]
fn create_with_connect_true() {
let sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p \
WITH (connect = true);";
let stmt = parse_one_create_stmt(sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&stmt, loc(), &mut acc).expect("ok");
let sub = acc.values().next().unwrap();
assert_eq!(
sub.options.connect,
Some(true),
"connect = true should parse to Some(true)"
);
}
#[test]
fn create_connstr_stored_verbatim() {
let sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x password=${PWD}' PUBLICATION p;";
let stmt = parse_one_create_stmt(sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&stmt, loc(), &mut acc).expect("ok");
let sub = acc.values().next().unwrap();
assert_eq!(sub.connection, "host=x password=${PWD}");
}
#[test]
fn alter_add_publication() {
let create_sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p;";
let alter_sql = "ALTER SUBSCRIPTION s ADD PUBLICATION q;";
let create = parse_one_create_stmt(create_sql);
let alter = parse_one_alter_stmt(alter_sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&create, loc(), &mut acc).expect("create ok");
parse_alter_subscription(&alter, loc(), &mut acc).expect("alter ok");
let sub = acc.values().next().unwrap();
assert_eq!(sub.publications.len(), 2);
let names: Vec<_> = sub
.publications
.iter()
.map(crate::identifier::Identifier::as_str)
.collect();
assert!(names.contains(&"p"));
assert!(names.contains(&"q"));
}
#[test]
fn alter_drop_publication() {
let create_sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p, q;";
let alter_sql = "ALTER SUBSCRIPTION s DROP PUBLICATION q;";
let create = parse_one_create_stmt(create_sql);
let alter = parse_one_alter_stmt(alter_sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&create, loc(), &mut acc).expect("create ok");
parse_alter_subscription(&alter, loc(), &mut acc).expect("alter ok");
let sub = acc.values().next().unwrap();
assert_eq!(sub.publications.len(), 1);
assert_eq!(sub.publications[0].as_str(), "p");
}
#[test]
fn alter_set_options() {
let create_sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p;";
let alter_sql = "ALTER SUBSCRIPTION s SET (binary = true);";
let create = parse_one_create_stmt(create_sql);
let alter = parse_one_alter_stmt(alter_sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&create, loc(), &mut acc).expect("create ok");
parse_alter_subscription(&alter, loc(), &mut acc).expect("alter ok");
let sub = acc.values().next().unwrap();
assert_eq!(sub.options.binary, Some(true));
}
#[test]
fn alter_connection() {
let create_sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p;";
let alter_sql = "ALTER SUBSCRIPTION s CONNECTION 'host=y';";
let create = parse_one_create_stmt(create_sql);
let alter = parse_one_alter_stmt(alter_sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&create, loc(), &mut acc).expect("create ok");
parse_alter_subscription(&alter, loc(), &mut acc).expect("alter ok");
let sub = acc.values().next().unwrap();
assert_eq!(sub.connection, "host=y");
}
#[test]
fn alter_refresh_publication_errors() {
let alter_sql = "ALTER SUBSCRIPTION s REFRESH PUBLICATION;";
let alter = parse_one_alter_stmt(alter_sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
let err = parse_alter_subscription(&alter, loc(), &mut acc).unwrap_err();
assert!(
matches!(err, ParseError::SubscriptionRefreshNotSupported(_, _)),
"got: {err:?}"
);
}
#[test]
fn alter_skip_errors() {
let alter_sql = "ALTER SUBSCRIPTION s SKIP (lsn = '0/0');";
let alter = parse_one_alter_stmt(alter_sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
let err = parse_alter_subscription(&alter, loc(), &mut acc).unwrap_err();
assert!(
matches!(err, ParseError::SubscriptionSkipNotSupported(_, _)),
"got: {err:?}"
);
}
#[test]
fn alter_enable_errors() {
let alter_sql = "ALTER SUBSCRIPTION s ENABLE;";
let alter = parse_one_alter_stmt(alter_sql);
let create_sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p;";
let create = parse_one_create_stmt(create_sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&create, loc(), &mut acc).expect("create ok");
let err = parse_alter_subscription(&alter, loc(), &mut acc).unwrap_err();
assert!(
matches!(
err,
ParseError::SubscriptionStandaloneEnableDisableNotSupported(_, _)
),
"got: {err:?}"
);
}
#[test]
fn rename_subscription_in_source_errors() {
let sql = "ALTER SUBSCRIPTION s RENAME TO t;";
let err = parse_source(sql).expect_err("should fail");
assert!(
matches!(
err,
ParseError::SubscriptionRenameNotSupported(_, _)
| ParseError::Structural { .. }
| ParseError::UnsupportedObjectKind { .. }
),
"got: {err:?}"
);
}
#[test]
fn create_empty_connection_errors() {
let mut stmt =
parse_one_create_stmt("CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p;");
stmt.conninfo = String::new();
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
let err = parse_create_subscription(&stmt, loc(), &mut acc).unwrap_err();
assert!(
matches!(err, ParseError::SubscriptionEmptyConnection(_, _)),
"got: {err:?}"
);
}
#[test]
fn create_empty_publications_errors() {
let mut stmt =
parse_one_create_stmt("CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p;");
stmt.publication.clear();
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
let err = parse_create_subscription(&stmt, loc(), &mut acc).unwrap_err();
assert!(
matches!(err, ParseError::SubscriptionEmptyPublications(_, _)),
"got: {err:?}"
);
}
#[test]
fn create_unknown_streaming_mode_errors() {
let mut stmt = parse_one_create_stmt(
"CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p WITH (streaming = on);",
);
if let Some(node) = stmt.options.iter_mut().next()
&& let Some(NodeEnum::DefElem(ref mut def)) = node.node
&& def.defname == "streaming"
{
def.arg = Some(Box::new(pg_query::protobuf::Node {
node: Some(NodeEnum::String(pg_query::protobuf::String {
sval: "bogus".to_string(),
})),
}));
}
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
let err = parse_create_subscription(&stmt, loc(), &mut acc).unwrap_err();
assert!(
matches!(err, ParseError::UnknownStreamingMode(ref s, _, _) if s == "bogus"),
"got: {err:?}"
);
}
#[test]
fn duplicate_subscription_errors() {
let sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p;";
let stmt = parse_one_create_stmt(sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
parse_create_subscription(&stmt, loc(), &mut acc).expect("first ok");
let err = parse_create_subscription(&stmt, loc(), &mut acc).unwrap_err();
assert!(
matches!(err, ParseError::DuplicateSubscription(_, _)),
"got: {err:?}"
);
}
#[test]
fn alter_before_create_errors() {
let alter_sql = "ALTER SUBSCRIPTION s ADD PUBLICATION q;";
let alter = parse_one_alter_stmt(alter_sql);
let mut acc: BTreeMap<Identifier, Subscription> = BTreeMap::new();
let err = parse_alter_subscription(&alter, loc(), &mut acc).unwrap_err();
assert!(
matches!(err, ParseError::AlterSubscriptionBeforeCreate(_, _)),
"got: {err:?}"
);
}
#[test]
fn parse_directory_create_subscription_minimal() {
let sql = "CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p;";
let cat = parse_source(sql).expect("parses");
assert_eq!(cat.subscriptions.len(), 1);
assert_eq!(cat.subscriptions[0].name.as_str(), "s");
assert_eq!(cat.subscriptions[0].connection, "host=x");
assert_eq!(cat.subscriptions[0].publications.len(), 1);
}
#[test]
fn parse_directory_folded_create_and_alter() {
let sql = "
CREATE SUBSCRIPTION s CONNECTION 'host=x' PUBLICATION p;
ALTER SUBSCRIPTION s ADD PUBLICATION q;
";
let cat = parse_source(sql).expect("parses");
assert_eq!(cat.subscriptions.len(), 1);
assert_eq!(cat.subscriptions[0].publications.len(), 2);
}
}