Skip to content

Commit d38dd78

Browse files
mjbshawiffyio
andauthored
Add support for PostgreSQL LOCK TABLE (#2273)
Co-authored-by: Ifeanyi Ubah <ify1992@yahoo.com>
1 parent 3fa7114 commit d38dd78

File tree

4 files changed

+237
-0
lines changed

4 files changed

+237
-0
lines changed

src/ast/mod.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4624,6 +4624,12 @@ pub enum Statement {
46244624
is_eq: bool,
46254625
},
46264626
/// ```sql
4627+
/// LOCK [ TABLE ] [ ONLY ] name [ * ] [, ...] [ IN lockmode MODE ] [ NOWAIT ]
4628+
/// ```
4629+
///
4630+
/// See <https://www.postgresql.org/docs/current/sql-lock.html>
4631+
Lock(Lock),
4632+
/// ```sql
46274633
/// LOCK TABLES <table_name> [READ [LOCAL] | [LOW_PRIORITY] WRITE]
46284634
/// ```
46294635
/// Note: this is a MySQL-specific statement. See <https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html>
@@ -4847,6 +4853,12 @@ impl From<ddl::Truncate> for Statement {
48474853
}
48484854
}
48494855

4856+
impl From<Lock> for Statement {
4857+
fn from(lock: Lock) -> Self {
4858+
Statement::Lock(lock)
4859+
}
4860+
}
4861+
48504862
impl From<ddl::Msck> for Statement {
48514863
fn from(msck: ddl::Msck) -> Self {
48524864
Statement::Msck(msck)
@@ -6141,6 +6153,7 @@ impl fmt::Display for Statement {
61416153
}
61426154
Ok(())
61436155
}
6156+
Statement::Lock(lock) => lock.fmt(f),
61446157
Statement::LockTables { tables } => {
61456158
write!(f, "LOCK TABLES {}", display_comma_separated(tables))
61466159
}
@@ -6387,6 +6400,104 @@ impl fmt::Display for TruncateTableTarget {
63876400
}
63886401
}
63896402

6403+
/// A `LOCK` statement.
6404+
///
6405+
/// See <https://www.postgresql.org/docs/current/sql-lock.html>
6406+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6407+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
6408+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6409+
pub struct Lock {
6410+
/// List of tables to lock.
6411+
pub tables: Vec<LockTableTarget>,
6412+
/// Lock mode.
6413+
pub lock_mode: Option<LockTableMode>,
6414+
/// Whether `NOWAIT` was specified.
6415+
pub nowait: bool,
6416+
}
6417+
6418+
impl fmt::Display for Lock {
6419+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6420+
write!(f, "LOCK TABLE {}", display_comma_separated(&self.tables))?;
6421+
if let Some(lock_mode) = &self.lock_mode {
6422+
write!(f, " IN {lock_mode} MODE")?;
6423+
}
6424+
if self.nowait {
6425+
write!(f, " NOWAIT")?;
6426+
}
6427+
Ok(())
6428+
}
6429+
}
6430+
6431+
/// Target of a `LOCK TABLE` command
6432+
///
6433+
/// See <https://www.postgresql.org/docs/current/sql-lock.html>
6434+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6435+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
6436+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6437+
pub struct LockTableTarget {
6438+
/// Name of the table being locked.
6439+
#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
6440+
pub name: ObjectName,
6441+
/// Whether `ONLY` was specified to exclude descendant tables.
6442+
pub only: bool,
6443+
/// Whether `*` was specified to explicitly include descendant tables.
6444+
pub has_asterisk: bool,
6445+
}
6446+
6447+
impl fmt::Display for LockTableTarget {
6448+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6449+
if self.only {
6450+
write!(f, "ONLY ")?;
6451+
}
6452+
write!(f, "{}", self.name)?;
6453+
if self.has_asterisk {
6454+
write!(f, " *")?;
6455+
}
6456+
Ok(())
6457+
}
6458+
}
6459+
6460+
/// PostgreSQL lock modes for `LOCK TABLE`.
6461+
///
6462+
/// See <https://www.postgresql.org/docs/current/sql-lock.html>
6463+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6464+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6465+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
6466+
pub enum LockTableMode {
6467+
/// `ACCESS SHARE`
6468+
AccessShare,
6469+
/// `ROW SHARE`
6470+
RowShare,
6471+
/// `ROW EXCLUSIVE`
6472+
RowExclusive,
6473+
/// `SHARE UPDATE EXCLUSIVE`
6474+
ShareUpdateExclusive,
6475+
/// `SHARE`
6476+
Share,
6477+
/// `SHARE ROW EXCLUSIVE`
6478+
ShareRowExclusive,
6479+
/// `EXCLUSIVE`
6480+
Exclusive,
6481+
/// `ACCESS EXCLUSIVE`
6482+
AccessExclusive,
6483+
}
6484+
6485+
impl fmt::Display for LockTableMode {
6486+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6487+
let text = match self {
6488+
Self::AccessShare => "ACCESS SHARE",
6489+
Self::RowShare => "ROW SHARE",
6490+
Self::RowExclusive => "ROW EXCLUSIVE",
6491+
Self::ShareUpdateExclusive => "SHARE UPDATE EXCLUSIVE",
6492+
Self::Share => "SHARE",
6493+
Self::ShareRowExclusive => "SHARE ROW EXCLUSIVE",
6494+
Self::Exclusive => "EXCLUSIVE",
6495+
Self::AccessExclusive => "ACCESS EXCLUSIVE",
6496+
};
6497+
write!(f, "{text}")
6498+
}
6499+
}
6500+
63906501
/// PostgreSQL identity option for TRUNCATE table
63916502
/// [ RESTART IDENTITY | CONTINUE IDENTITY ]
63926503
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]

src/ast/spans.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ impl Spanned for Values {
304304
/// - [Statement::CreateSequence]
305305
/// - [Statement::CreateType]
306306
/// - [Statement::Pragma]
307+
/// - [Statement::Lock]
307308
/// - [Statement::LockTables]
308309
/// - [Statement::UnlockTables]
309310
/// - [Statement::Unload]
@@ -462,6 +463,7 @@ impl Spanned for Statement {
462463
Statement::CreateSequence { .. } => Span::empty(),
463464
Statement::CreateType { .. } => Span::empty(),
464465
Statement::Pragma { .. } => Span::empty(),
466+
Statement::Lock(_) => Span::empty(),
465467
Statement::LockTables { .. } => Span::empty(),
466468
Statement::UnlockTables => Span::empty(),
467469
Statement::Unload { .. } => Span::empty(),

src/parser/mod.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,10 @@ impl<'a> Parser<'a> {
697697
// `INSTALL` is duckdb specific https://duckdb.org/docs/extensions/overview
698698
Keyword::INSTALL if self.dialect.supports_install() => self.parse_install(),
699699
Keyword::LOAD => self.parse_load(),
700+
Keyword::LOCK => {
701+
self.prev_token();
702+
self.parse_lock_statement().map(Into::into)
703+
}
700704
Keyword::OPTIMIZE if self.dialect.supports_optimize_table() => {
701705
self.parse_optimize_table()
702706
}
@@ -18389,6 +18393,66 @@ impl<'a> Parser<'a> {
1838918393
})
1839018394
}
1839118395

18396+
/// Parse a PostgreSQL `LOCK` statement.
18397+
pub fn parse_lock_statement(&mut self) -> Result<Lock, ParserError> {
18398+
self.expect_keyword(Keyword::LOCK)?;
18399+
18400+
if self.peek_keyword(Keyword::TABLES) {
18401+
return self.expected_ref("TABLE or a table name", self.peek_token_ref());
18402+
}
18403+
18404+
let _ = self.parse_keyword(Keyword::TABLE);
18405+
let tables = self.parse_comma_separated(Parser::parse_lock_table_target)?;
18406+
let lock_mode = if self.parse_keyword(Keyword::IN) {
18407+
let lock_mode = self.parse_lock_table_mode()?;
18408+
self.expect_keyword(Keyword::MODE)?;
18409+
Some(lock_mode)
18410+
} else {
18411+
None
18412+
};
18413+
let nowait = self.parse_keyword(Keyword::NOWAIT);
18414+
18415+
Ok(Lock {
18416+
tables,
18417+
lock_mode,
18418+
nowait,
18419+
})
18420+
}
18421+
18422+
fn parse_lock_table_target(&mut self) -> Result<LockTableTarget, ParserError> {
18423+
let only = self.parse_keyword(Keyword::ONLY);
18424+
let name = self.parse_object_name(false)?;
18425+
let has_asterisk = self.consume_token(&Token::Mul);
18426+
18427+
Ok(LockTableTarget {
18428+
name,
18429+
only,
18430+
has_asterisk,
18431+
})
18432+
}
18433+
18434+
fn parse_lock_table_mode(&mut self) -> Result<LockTableMode, ParserError> {
18435+
if self.parse_keywords(&[Keyword::ACCESS, Keyword::SHARE]) {
18436+
Ok(LockTableMode::AccessShare)
18437+
} else if self.parse_keywords(&[Keyword::ACCESS, Keyword::EXCLUSIVE]) {
18438+
Ok(LockTableMode::AccessExclusive)
18439+
} else if self.parse_keywords(&[Keyword::ROW, Keyword::SHARE]) {
18440+
Ok(LockTableMode::RowShare)
18441+
} else if self.parse_keywords(&[Keyword::ROW, Keyword::EXCLUSIVE]) {
18442+
Ok(LockTableMode::RowExclusive)
18443+
} else if self.parse_keywords(&[Keyword::SHARE, Keyword::UPDATE, Keyword::EXCLUSIVE]) {
18444+
Ok(LockTableMode::ShareUpdateExclusive)
18445+
} else if self.parse_keywords(&[Keyword::SHARE, Keyword::ROW, Keyword::EXCLUSIVE]) {
18446+
Ok(LockTableMode::ShareRowExclusive)
18447+
} else if self.parse_keyword(Keyword::SHARE) {
18448+
Ok(LockTableMode::Share)
18449+
} else if self.parse_keyword(Keyword::EXCLUSIVE) {
18450+
Ok(LockTableMode::Exclusive)
18451+
} else {
18452+
self.expected_ref("a PostgreSQL LOCK TABLE mode", self.peek_token_ref())
18453+
}
18454+
}
18455+
1839218456
/// Parse a VALUES clause
1839318457
pub fn parse_values(
1839418458
&mut self,

tests/sqlparser_postgres.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8703,3 +8703,63 @@ fn parse_pg_analyze() {
87038703
_ => panic!("Expected Analyze, got: {stmt:?}"),
87048704
}
87058705
}
8706+
8707+
#[test]
8708+
fn parse_lock_table() {
8709+
pg_and_generic().one_statement_parses_to(
8710+
"LOCK public.widgets IN EXCLUSIVE MODE",
8711+
"LOCK TABLE public.widgets IN EXCLUSIVE MODE",
8712+
);
8713+
pg_and_generic().one_statement_parses_to(
8714+
"LOCK TABLE public.widgets NOWAIT",
8715+
"LOCK TABLE public.widgets NOWAIT",
8716+
);
8717+
8718+
let stmt = pg_and_generic().verified_stmt(
8719+
"LOCK TABLE ONLY public.widgets, analytics.events * IN SHARE ROW EXCLUSIVE MODE NOWAIT",
8720+
);
8721+
match stmt {
8722+
Statement::Lock(lock) => {
8723+
assert_eq!(lock.tables.len(), 2);
8724+
assert_eq!(lock.tables[0].name.to_string(), "public.widgets");
8725+
assert!(lock.tables[0].only);
8726+
assert!(!lock.tables[0].has_asterisk);
8727+
assert_eq!(lock.tables[1].name.to_string(), "analytics.events");
8728+
assert!(!lock.tables[1].only);
8729+
assert!(lock.tables[1].has_asterisk);
8730+
assert_eq!(lock.lock_mode, Some(LockTableMode::ShareRowExclusive));
8731+
assert!(lock.nowait);
8732+
}
8733+
_ => panic!("Expected Lock, got: {stmt:?}"),
8734+
}
8735+
8736+
let lock_modes = [
8737+
("ACCESS SHARE", LockTableMode::AccessShare),
8738+
("ROW SHARE", LockTableMode::RowShare),
8739+
("ROW EXCLUSIVE", LockTableMode::RowExclusive),
8740+
(
8741+
"SHARE UPDATE EXCLUSIVE",
8742+
LockTableMode::ShareUpdateExclusive,
8743+
),
8744+
("SHARE", LockTableMode::Share),
8745+
("SHARE ROW EXCLUSIVE", LockTableMode::ShareRowExclusive),
8746+
("EXCLUSIVE", LockTableMode::Exclusive),
8747+
("ACCESS EXCLUSIVE", LockTableMode::AccessExclusive),
8748+
];
8749+
8750+
for (mode_sql, expected_mode) in lock_modes {
8751+
let stmt = pg_and_generic()
8752+
.verified_stmt(&format!("LOCK TABLE public.widgets IN {mode_sql} MODE"));
8753+
match stmt {
8754+
Statement::Lock(lock) => {
8755+
assert_eq!(lock.tables.len(), 1);
8756+
assert_eq!(lock.tables[0].name.to_string(), "public.widgets");
8757+
assert!(!lock.tables[0].only);
8758+
assert!(!lock.tables[0].has_asterisk);
8759+
assert_eq!(lock.lock_mode, Some(expected_mode));
8760+
assert!(!lock.nowait);
8761+
}
8762+
_ => panic!("Expected Lock, got: {stmt:?}"),
8763+
}
8764+
}
8765+
}

0 commit comments

Comments
 (0)