Skip to content

Commit 8a534c0

Browse files
authored
Implements CREATE POLICY syntax for PostgreSQL (#1440)
1 parent affe8b5 commit 8a534c0

File tree

4 files changed

+292
-15
lines changed

4 files changed

+292
-15
lines changed

src/ast/mod.rs

+85
Original file line numberDiff line numberDiff line change
@@ -2135,6 +2135,35 @@ pub enum FromTable {
21352135
WithoutKeyword(Vec<TableWithJoins>),
21362136
}
21372137

2138+
/// Policy type for a `CREATE POLICY` statement.
2139+
/// ```sql
2140+
/// AS [ PERMISSIVE | RESTRICTIVE ]
2141+
/// ```
2142+
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html)
2143+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
2144+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2145+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
2146+
pub enum CreatePolicyType {
2147+
Permissive,
2148+
Restrictive,
2149+
}
2150+
2151+
/// Policy command for a `CREATE POLICY` statement.
2152+
/// ```sql
2153+
/// FOR [ALL | SELECT | INSERT | UPDATE | DELETE]
2154+
/// ```
2155+
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html)
2156+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
2157+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2158+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
2159+
pub enum CreatePolicyCommand {
2160+
All,
2161+
Select,
2162+
Insert,
2163+
Update,
2164+
Delete,
2165+
}
2166+
21382167
/// A top-level statement (SELECT, INSERT, CREATE, etc.)
21392168
#[allow(clippy::large_enum_variant)]
21402169
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -2375,6 +2404,20 @@ pub enum Statement {
23752404
options: Vec<SecretOption>,
23762405
},
23772406
/// ```sql
2407+
/// CREATE POLICY
2408+
/// ```
2409+
/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html)
2410+
CreatePolicy {
2411+
name: Ident,
2412+
#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
2413+
table_name: ObjectName,
2414+
policy_type: Option<CreatePolicyType>,
2415+
command: Option<CreatePolicyCommand>,
2416+
to: Option<Vec<Owner>>,
2417+
using: Option<Expr>,
2418+
with_check: Option<Expr>,
2419+
},
2420+
/// ```sql
23782421
/// ALTER TABLE
23792422
/// ```
23802423
AlterTable {
@@ -4052,6 +4095,48 @@ impl fmt::Display for Statement {
40524095
write!(f, " )")?;
40534096
Ok(())
40544097
}
4098+
Statement::CreatePolicy {
4099+
name,
4100+
table_name,
4101+
policy_type,
4102+
command,
4103+
to,
4104+
using,
4105+
with_check,
4106+
} => {
4107+
write!(f, "CREATE POLICY {name} ON {table_name}")?;
4108+
4109+
if let Some(policy_type) = policy_type {
4110+
match policy_type {
4111+
CreatePolicyType::Permissive => write!(f, " AS PERMISSIVE")?,
4112+
CreatePolicyType::Restrictive => write!(f, " AS RESTRICTIVE")?,
4113+
}
4114+
}
4115+
4116+
if let Some(command) = command {
4117+
match command {
4118+
CreatePolicyCommand::All => write!(f, " FOR ALL")?,
4119+
CreatePolicyCommand::Select => write!(f, " FOR SELECT")?,
4120+
CreatePolicyCommand::Insert => write!(f, " FOR INSERT")?,
4121+
CreatePolicyCommand::Update => write!(f, " FOR UPDATE")?,
4122+
CreatePolicyCommand::Delete => write!(f, " FOR DELETE")?,
4123+
}
4124+
}
4125+
4126+
if let Some(to) = to {
4127+
write!(f, " TO {}", display_comma_separated(to))?;
4128+
}
4129+
4130+
if let Some(using) = using {
4131+
write!(f, " USING ({using})")?;
4132+
}
4133+
4134+
if let Some(with_check) = with_check {
4135+
write!(f, " WITH CHECK ({with_check})")?;
4136+
}
4137+
4138+
Ok(())
4139+
}
40554140
Statement::AlterTable {
40564141
name,
40574142
if_exists,

src/keywords.rs

+2
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ define_keywords!(
568568
PERCENTILE_DISC,
569569
PERCENT_RANK,
570570
PERIOD,
571+
PERMISSIVE,
571572
PERSISTENT,
572573
PIVOT,
573574
PLACING,
@@ -634,6 +635,7 @@ define_keywords!(
634635
RESTART,
635636
RESTRICT,
636637
RESTRICTED,
638+
RESTRICTIVE,
637639
RESULT,
638640
RESULTSET,
639641
RETAIN,

src/parser/mod.rs

+103-15
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use IsLateral::*;
3232
use IsOptional::*;
3333

3434
use crate::ast::helpers::stmt_create_table::{CreateTableBuilder, CreateTableConfiguration};
35+
use crate::ast::Statement::CreatePolicy;
3536
use crate::ast::*;
3637
use crate::dialect::*;
3738
use crate::keywords::{Keyword, ALL_KEYWORDS};
@@ -3569,6 +3570,8 @@ impl<'a> Parser<'a> {
35693570
} else if self.parse_keyword(Keyword::MATERIALIZED) || self.parse_keyword(Keyword::VIEW) {
35703571
self.prev_token();
35713572
self.parse_create_view(or_replace, temporary)
3573+
} else if self.parse_keyword(Keyword::POLICY) {
3574+
self.parse_create_policy()
35723575
} else if self.parse_keyword(Keyword::EXTERNAL) {
35733576
self.parse_create_external_table(or_replace)
35743577
} else if self.parse_keyword(Keyword::FUNCTION) {
@@ -4762,6 +4765,105 @@ impl<'a> Parser<'a> {
47624765
})
47634766
}
47644767

4768+
pub fn parse_owner(&mut self) -> Result<Owner, ParserError> {
4769+
let owner = match self.parse_one_of_keywords(&[Keyword::CURRENT_USER, Keyword::CURRENT_ROLE, Keyword::SESSION_USER]) {
4770+
Some(Keyword::CURRENT_USER) => Owner::CurrentUser,
4771+
Some(Keyword::CURRENT_ROLE) => Owner::CurrentRole,
4772+
Some(Keyword::SESSION_USER) => Owner::SessionUser,
4773+
Some(_) => unreachable!(),
4774+
None => {
4775+
match self.parse_identifier(false) {
4776+
Ok(ident) => Owner::Ident(ident),
4777+
Err(e) => {
4778+
return Err(ParserError::ParserError(format!("Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. {e}")))
4779+
}
4780+
}
4781+
},
4782+
};
4783+
Ok(owner)
4784+
}
4785+
4786+
/// ```sql
4787+
/// CREATE POLICY name ON table_name [ AS { PERMISSIVE | RESTRICTIVE } ]
4788+
/// [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ]
4789+
/// [ TO { role_name | PUBLIC | CURRENT_USER | CURRENT_ROLE | SESSION_USER } [, ...] ]
4790+
/// [ USING ( using_expression ) ]
4791+
/// [ WITH CHECK ( with_check_expression ) ]
4792+
/// ```
4793+
///
4794+
/// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createpolicy.html)
4795+
pub fn parse_create_policy(&mut self) -> Result<Statement, ParserError> {
4796+
let name = self.parse_identifier(false)?;
4797+
self.expect_keyword(Keyword::ON)?;
4798+
let table_name = self.parse_object_name(false)?;
4799+
4800+
let policy_type = if self.parse_keyword(Keyword::AS) {
4801+
let keyword =
4802+
self.expect_one_of_keywords(&[Keyword::PERMISSIVE, Keyword::RESTRICTIVE])?;
4803+
Some(match keyword {
4804+
Keyword::PERMISSIVE => CreatePolicyType::Permissive,
4805+
Keyword::RESTRICTIVE => CreatePolicyType::Restrictive,
4806+
_ => unreachable!(),
4807+
})
4808+
} else {
4809+
None
4810+
};
4811+
4812+
let command = if self.parse_keyword(Keyword::FOR) {
4813+
let keyword = self.expect_one_of_keywords(&[
4814+
Keyword::ALL,
4815+
Keyword::SELECT,
4816+
Keyword::INSERT,
4817+
Keyword::UPDATE,
4818+
Keyword::DELETE,
4819+
])?;
4820+
Some(match keyword {
4821+
Keyword::ALL => CreatePolicyCommand::All,
4822+
Keyword::SELECT => CreatePolicyCommand::Select,
4823+
Keyword::INSERT => CreatePolicyCommand::Insert,
4824+
Keyword::UPDATE => CreatePolicyCommand::Update,
4825+
Keyword::DELETE => CreatePolicyCommand::Delete,
4826+
_ => unreachable!(),
4827+
})
4828+
} else {
4829+
None
4830+
};
4831+
4832+
let to = if self.parse_keyword(Keyword::TO) {
4833+
Some(self.parse_comma_separated(|p| p.parse_owner())?)
4834+
} else {
4835+
None
4836+
};
4837+
4838+
let using = if self.parse_keyword(Keyword::USING) {
4839+
self.expect_token(&Token::LParen)?;
4840+
let expr = self.parse_expr()?;
4841+
self.expect_token(&Token::RParen)?;
4842+
Some(expr)
4843+
} else {
4844+
None
4845+
};
4846+
4847+
let with_check = if self.parse_keywords(&[Keyword::WITH, Keyword::CHECK]) {
4848+
self.expect_token(&Token::LParen)?;
4849+
let expr = self.parse_expr()?;
4850+
self.expect_token(&Token::RParen)?;
4851+
Some(expr)
4852+
} else {
4853+
None
4854+
};
4855+
4856+
Ok(CreatePolicy {
4857+
name,
4858+
table_name,
4859+
policy_type,
4860+
command,
4861+
to,
4862+
using,
4863+
with_check,
4864+
})
4865+
}
4866+
47654867
pub fn parse_drop(&mut self) -> Result<Statement, ParserError> {
47664868
// MySQL dialect supports `TEMPORARY`
47674869
let temporary = dialect_of!(self is MySqlDialect | GenericDialect | DuckDbDialect)
@@ -6941,21 +7043,7 @@ impl<'a> Parser<'a> {
69417043
} else if dialect_of!(self is PostgreSqlDialect | GenericDialect)
69427044
&& self.parse_keywords(&[Keyword::OWNER, Keyword::TO])
69437045
{
6944-
let new_owner = match self.parse_one_of_keywords(&[Keyword::CURRENT_USER, Keyword::CURRENT_ROLE, Keyword::SESSION_USER]) {
6945-
Some(Keyword::CURRENT_USER) => Owner::CurrentUser,
6946-
Some(Keyword::CURRENT_ROLE) => Owner::CurrentRole,
6947-
Some(Keyword::SESSION_USER) => Owner::SessionUser,
6948-
Some(_) => unreachable!(),
6949-
None => {
6950-
match self.parse_identifier(false) {
6951-
Ok(ident) => Owner::Ident(ident),
6952-
Err(e) => {
6953-
return Err(ParserError::ParserError(format!("Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. {e}")))
6954-
}
6955-
}
6956-
},
6957-
};
6958-
7046+
let new_owner = self.parse_owner()?;
69597047
AlterTableOperation::OwnerTo { new_owner }
69607048
} else if dialect_of!(self is ClickHouseDialect|GenericDialect)
69617049
&& self.parse_keyword(Keyword::ATTACH)

tests/sqlparser_common.rs

+102
Original file line numberDiff line numberDiff line change
@@ -10987,3 +10987,105 @@ fn parse_explain_with_option_list() {
1098710987
Some(utility_options),
1098810988
);
1098910989
}
10990+
10991+
#[test]
10992+
fn test_create_policy() {
10993+
let sql = concat!(
10994+
"CREATE POLICY my_policy ON my_table ",
10995+
"AS PERMISSIVE FOR SELECT ",
10996+
"TO my_role, CURRENT_USER ",
10997+
"USING (c0 = 1) ",
10998+
"WITH CHECK (true)"
10999+
);
11000+
11001+
match all_dialects().verified_stmt(sql) {
11002+
Statement::CreatePolicy {
11003+
name,
11004+
table_name,
11005+
to,
11006+
using,
11007+
with_check,
11008+
..
11009+
} => {
11010+
assert_eq!(name.to_string(), "my_policy");
11011+
assert_eq!(table_name.to_string(), "my_table");
11012+
assert_eq!(
11013+
to,
11014+
Some(vec![
11015+
Owner::Ident(Ident::new("my_role")),
11016+
Owner::CurrentUser
11017+
])
11018+
);
11019+
assert_eq!(
11020+
using,
11021+
Some(Expr::BinaryOp {
11022+
left: Box::new(Expr::Identifier(Ident::new("c0"))),
11023+
op: BinaryOperator::Eq,
11024+
right: Box::new(Expr::Value(Value::Number("1".parse().unwrap(), false))),
11025+
})
11026+
);
11027+
assert_eq!(with_check, Some(Expr::Value(Value::Boolean(true))));
11028+
}
11029+
_ => unreachable!(),
11030+
}
11031+
11032+
// USING with SELECT query
11033+
all_dialects().verified_stmt(concat!(
11034+
"CREATE POLICY my_policy ON my_table ",
11035+
"AS PERMISSIVE FOR SELECT ",
11036+
"TO my_role, CURRENT_USER ",
11037+
"USING (c0 IN (SELECT column FROM t0)) ",
11038+
"WITH CHECK (true)"
11039+
));
11040+
// omit AS / FOR / TO / USING / WITH CHECK clauses is allowed
11041+
all_dialects().verified_stmt("CREATE POLICY my_policy ON my_table");
11042+
11043+
// missing table name
11044+
assert_eq!(
11045+
all_dialects()
11046+
.parse_sql_statements("CREATE POLICY my_policy")
11047+
.unwrap_err()
11048+
.to_string(),
11049+
"sql parser error: Expected: ON, found: EOF"
11050+
);
11051+
// missing policy type
11052+
assert_eq!(
11053+
all_dialects()
11054+
.parse_sql_statements("CREATE POLICY my_policy ON my_table AS")
11055+
.unwrap_err()
11056+
.to_string(),
11057+
"sql parser error: Expected: one of PERMISSIVE or RESTRICTIVE, found: EOF"
11058+
);
11059+
// missing FOR command
11060+
assert_eq!(
11061+
all_dialects()
11062+
.parse_sql_statements("CREATE POLICY my_policy ON my_table FOR")
11063+
.unwrap_err()
11064+
.to_string(),
11065+
"sql parser error: Expected: one of ALL or SELECT or INSERT or UPDATE or DELETE, found: EOF"
11066+
);
11067+
// missing TO owners
11068+
assert_eq!(
11069+
all_dialects()
11070+
.parse_sql_statements("CREATE POLICY my_policy ON my_table TO")
11071+
.unwrap_err()
11072+
.to_string(),
11073+
"sql parser error: Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. sql parser error: Expected: identifier, found: EOF"
11074+
);
11075+
// missing USING expression
11076+
assert_eq!(
11077+
all_dialects()
11078+
.parse_sql_statements("CREATE POLICY my_policy ON my_table USING")
11079+
.unwrap_err()
11080+
.to_string(),
11081+
"sql parser error: Expected: (, found: EOF"
11082+
);
11083+
// missing WITH CHECK expression
11084+
assert_eq!(
11085+
all_dialects()
11086+
.parse_sql_statements("CREATE POLICY my_policy ON my_table WITH CHECK")
11087+
.unwrap_err()
11088+
.to_string(),
11089+
"sql parser error: Expected: (, found: EOF"
11090+
);
11091+
}

0 commit comments

Comments
 (0)