Skip to content

Commit cd9919b

Browse files
committed
Parse Postgres's LOCK TABLE statement
See: https://www.postgresql.org/docs/current/sql-lock.html PG's full syntax for this statement is supported: ``` LOCK [ TABLE ] [ ONLY ] name [ * ] [, ...] [ IN lockmode MODE ] [ NOWAIT ] where lockmode is one of: ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE ``` MySQL and Postgres have support very different syntax for `LOCK TABLE` and are implemented with a breaking change on the `Statement::LockTables { .. }` variant, turning the variant into one which accepts a `LockTables` enum with variants for MySQL and Posgres.
1 parent 94ea206 commit cd9919b

File tree

5 files changed

+277
-29
lines changed

5 files changed

+277
-29
lines changed

src/ast/mod.rs

+167-15
Original file line numberDiff line numberDiff line change
@@ -3341,16 +3341,13 @@ pub enum Statement {
33413341
value: Option<Value>,
33423342
is_eq: bool,
33433343
},
3344-
/// ```sql
3345-
/// LOCK TABLES <table_name> [READ [LOCAL] | [LOW_PRIORITY] WRITE]
3346-
/// ```
3347-
/// Note: this is a MySQL-specific statement. See <https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html>
3348-
LockTables { tables: Vec<LockTable> },
3344+
/// See [`LockTables`].
3345+
LockTables(LockTables),
33493346
/// ```sql
33503347
/// UNLOCK TABLES
33513348
/// ```
33523349
/// Note: this is a MySQL-specific statement. See <https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html>
3353-
UnlockTables,
3350+
UnlockTables(bool),
33543351
/// ```sql
33553352
/// UNLOAD(statement) TO <destination> [ WITH options ]
33563353
/// ```
@@ -4925,11 +4922,15 @@ impl fmt::Display for Statement {
49254922
}
49264923
Ok(())
49274924
}
4928-
Statement::LockTables { tables } => {
4929-
write!(f, "LOCK TABLES {}", display_comma_separated(tables))
4925+
Statement::LockTables(lock_tables) => {
4926+
write!(f, "{}", lock_tables)
49304927
}
4931-
Statement::UnlockTables => {
4932-
write!(f, "UNLOCK TABLES")
4928+
Statement::UnlockTables(pluralized) => {
4929+
if *pluralized {
4930+
write!(f, "UNLOCK TABLES")
4931+
} else {
4932+
write!(f, "UNLOCK TABLE")
4933+
}
49334934
}
49344935
Statement::Unload { query, to, with } => {
49354936
write!(f, "UNLOAD({query}) TO {to}")?;
@@ -7278,16 +7279,126 @@ impl fmt::Display for SearchModifier {
72787279
}
72797280
}
72807281

7282+
/// A `LOCK TABLE ..` statement. MySQL and Postgres variants are supported.
7283+
///
7284+
/// The MySQL and Postgres syntax variants are significant enough that they
7285+
/// are explicitly represented as enum variants. In order to support additional
7286+
/// databases in the future, this enum is marked as `#[non_exhaustive]`.
7287+
///
7288+
/// In MySQL, when multiple tables are mentioned in the statement the lock mode
7289+
/// can vary per table.
7290+
///
7291+
/// In contrast, Postgres only allows specifying a single mode which is applied
7292+
/// to all mentioned tables.
7293+
///
7294+
/// MySQL: see <https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html>
7295+
///
7296+
/// ```sql
7297+
/// LOCK [TABLE | TABLES] name [[AS] alias] locktype [,name [[AS] alias] locktype]
7298+
/// ````
7299+
///
7300+
/// Where *locktype* is:
7301+
/// ```sql
7302+
/// READ [LOCAL] | [LOW_PRIORITY] WRITE
7303+
/// ```
7304+
///
7305+
/// Postgres: See <https://www.postgresql.org/docs/current/sql-lock.html>
7306+
///
7307+
/// ```sql
7308+
/// LOCK [ TABLE ] [ ONLY ] name [, ...] [ IN lockmode MODE ] [ NOWAIT ]
7309+
/// ```
7310+
/// Where *lockmode* is one of:
7311+
///
7312+
/// ```sql
7313+
/// ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE
7314+
/// | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE
7315+
/// ``````
72817316
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
72827317
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
72837318
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
7284-
pub struct LockTable {
7285-
pub table: Ident,
7319+
#[non_exhaustive]
7320+
pub enum LockTables {
7321+
/// The MySQL syntax variant
7322+
MySql {
7323+
/// Whether the `TABLE` or `TABLES` keyword was used.
7324+
pluralized_table_keyword: bool,
7325+
/// The tables to lock and their per-table lock mode.
7326+
tables: Vec<MySqlTableLock>,
7327+
},
7328+
7329+
/// The Postgres syntax variant.
7330+
Postgres {
7331+
/// One or more optionally schema-qualified table names to lock.
7332+
tables: Vec<ObjectName>,
7333+
/// The lock type applied to all mentioned tables.
7334+
lock_mode: Option<LockTableType>,
7335+
/// Whether the optional `TABLE` keyword was present (to support round-trip parse & render)
7336+
has_table_keyword: bool,
7337+
/// Whether the `ONLY` option was specified.
7338+
only: bool,
7339+
/// Whether the `NOWAIT` option was specified.
7340+
no_wait: bool,
7341+
},
7342+
}
7343+
7344+
impl Display for LockTables {
7345+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7346+
match self {
7347+
LockTables::MySql {
7348+
pluralized_table_keyword,
7349+
tables,
7350+
} => {
7351+
write!(
7352+
f,
7353+
"LOCK {tbl_kwd} ",
7354+
tbl_kwd = if *pluralized_table_keyword {
7355+
"TABLES"
7356+
} else {
7357+
"TABLE"
7358+
}
7359+
)?;
7360+
write!(f, "{}", display_comma_separated(tables))?;
7361+
Ok(())
7362+
}
7363+
LockTables::Postgres {
7364+
tables,
7365+
lock_mode,
7366+
has_table_keyword,
7367+
only,
7368+
no_wait,
7369+
} => {
7370+
write!(
7371+
f,
7372+
"LOCK{tbl_kwd}",
7373+
tbl_kwd = if *has_table_keyword { " TABLE" } else { "" }
7374+
)?;
7375+
if *only {
7376+
write!(f, " ONLY")?;
7377+
}
7378+
write!(f, " {}", display_comma_separated(tables))?;
7379+
if let Some(lock_mode) = lock_mode {
7380+
write!(f, " IN {} MODE", lock_mode)?;
7381+
}
7382+
if *no_wait {
7383+
write!(f, " NOWAIT")?;
7384+
}
7385+
Ok(())
7386+
}
7387+
}
7388+
}
7389+
}
7390+
7391+
/// A locked table from a MySQL `LOCK TABLE` statement.
7392+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
7393+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7394+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
7395+
pub struct MySqlTableLock {
7396+
pub table: ObjectName,
72867397
pub alias: Option<Ident>,
7287-
pub lock_type: LockTableType,
7398+
pub lock_type: Option<LockTableType>,
72887399
}
72897400

7290-
impl fmt::Display for LockTable {
7401+
impl fmt::Display for MySqlTableLock {
72917402
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72927403
let Self {
72937404
table: tbl_name,
@@ -7299,17 +7410,34 @@ impl fmt::Display for LockTable {
72997410
if let Some(alias) = alias {
73007411
write!(f, "AS {alias} ")?;
73017412
}
7302-
write!(f, "{lock_type}")?;
7413+
if let Some(lock_type) = lock_type {
7414+
write!(f, "{lock_type}")?;
7415+
}
73037416
Ok(())
73047417
}
73057418
}
73067419

7420+
/// Table lock types.
7421+
///
7422+
/// `Read` & `Write` are MySQL-specfic.
7423+
///
7424+
/// AccessShare, RowShare, RowExclusive, ShareUpdateExclusive, Share,
7425+
/// ShareRowExclusive, Exclusive, AccessExclusive are Postgres-specific.
73077426
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
73087427
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
73097428
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
7429+
#[non_exhaustive]
73107430
pub enum LockTableType {
73117431
Read { local: bool },
73127432
Write { low_priority: bool },
7433+
AccessShare,
7434+
RowShare,
7435+
RowExclusive,
7436+
ShareUpdateExclusive,
7437+
Share,
7438+
ShareRowExclusive,
7439+
Exclusive,
7440+
AccessExclusive,
73137441
}
73147442

73157443
impl fmt::Display for LockTableType {
@@ -7327,6 +7455,30 @@ impl fmt::Display for LockTableType {
73277455
}
73287456
write!(f, "WRITE")?;
73297457
}
7458+
Self::AccessShare => {
7459+
write!(f, "ACCESS SHARE")?;
7460+
}
7461+
Self::RowShare => {
7462+
write!(f, "ROW SHARE")?;
7463+
}
7464+
Self::RowExclusive => {
7465+
write!(f, "ROW EXCLUSIVE")?;
7466+
}
7467+
Self::ShareUpdateExclusive => {
7468+
write!(f, "SHARE UPDATE EXCLUSIVE")?;
7469+
}
7470+
Self::Share => {
7471+
write!(f, "SHARE")?;
7472+
}
7473+
Self::ShareRowExclusive => {
7474+
write!(f, "SHARE ROW EXCLUSIVE")?;
7475+
}
7476+
Self::Exclusive => {
7477+
write!(f, "EXCLUSIVE")?;
7478+
}
7479+
Self::AccessExclusive => {
7480+
write!(f, "ACCESS EXCLUSIVE")?;
7481+
}
73307482
}
73317483

73327484
Ok(())

src/ast/spans.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ impl Spanned for Statement {
479479
Statement::CreateType { .. } => Span::empty(),
480480
Statement::Pragma { .. } => Span::empty(),
481481
Statement::LockTables { .. } => Span::empty(),
482-
Statement::UnlockTables => Span::empty(),
482+
Statement::UnlockTables(_) => Span::empty(),
483483
Statement::Unload { .. } => Span::empty(),
484484
Statement::OptimizeTable { .. } => Span::empty(),
485485
Statement::CreatePolicy { .. } => Span::empty(),

src/dialect/mysql.rs

+25-12
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
use alloc::boxed::Box;
2020

2121
use crate::{
22-
ast::{BinaryOperator, Expr, LockTable, LockTableType, Statement},
22+
ast::{BinaryOperator, Expr, LockTableType, LockTables, MySqlTableLock, Statement},
2323
dialect::Dialect,
2424
keywords::Keyword,
2525
parser::{Parser, ParserError},
@@ -81,10 +81,14 @@ impl Dialect for MySqlDialect {
8181
}
8282

8383
fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
84-
if parser.parse_keywords(&[Keyword::LOCK, Keyword::TABLES]) {
85-
Some(parse_lock_tables(parser))
84+
if parser.parse_keywords(&[Keyword::LOCK, Keyword::TABLE]) {
85+
Some(parse_lock_tables(parser, false))
86+
} else if parser.parse_keywords(&[Keyword::LOCK, Keyword::TABLES]) {
87+
Some(parse_lock_tables(parser, true))
88+
} else if parser.parse_keywords(&[Keyword::UNLOCK, Keyword::TABLE]) {
89+
Some(parse_unlock_tables(parser, false))
8690
} else if parser.parse_keywords(&[Keyword::UNLOCK, Keyword::TABLES]) {
87-
Some(parse_unlock_tables(parser))
91+
Some(parse_unlock_tables(parser, true))
8892
} else {
8993
None
9094
}
@@ -106,22 +110,28 @@ impl Dialect for MySqlDialect {
106110

107111
/// `LOCK TABLES`
108112
/// <https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html>
109-
fn parse_lock_tables(parser: &mut Parser) -> Result<Statement, ParserError> {
113+
fn parse_lock_tables(
114+
parser: &mut Parser,
115+
pluralized_table_keyword: bool,
116+
) -> Result<Statement, ParserError> {
110117
let tables = parser.parse_comma_separated(parse_lock_table)?;
111-
Ok(Statement::LockTables { tables })
118+
Ok(Statement::LockTables(LockTables::MySql {
119+
pluralized_table_keyword,
120+
tables,
121+
}))
112122
}
113123

114124
// tbl_name [[AS] alias] lock_type
115-
fn parse_lock_table(parser: &mut Parser) -> Result<LockTable, ParserError> {
116-
let table = parser.parse_identifier()?;
125+
fn parse_lock_table(parser: &mut Parser) -> Result<MySqlTableLock, ParserError> {
126+
let table = parser.parse_object_name(false)?;
117127
let alias =
118128
parser.parse_optional_alias(&[Keyword::READ, Keyword::WRITE, Keyword::LOW_PRIORITY])?;
119129
let lock_type = parse_lock_tables_type(parser)?;
120130

121-
Ok(LockTable {
131+
Ok(MySqlTableLock {
122132
table,
123133
alias,
124-
lock_type,
134+
lock_type: Some(lock_type),
125135
})
126136
}
127137

@@ -146,6 +156,9 @@ fn parse_lock_tables_type(parser: &mut Parser) -> Result<LockTableType, ParserEr
146156

147157
/// UNLOCK TABLES
148158
/// <https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html>
149-
fn parse_unlock_tables(_parser: &mut Parser) -> Result<Statement, ParserError> {
150-
Ok(Statement::UnlockTables)
159+
fn parse_unlock_tables(
160+
_parser: &mut Parser,
161+
pluralized_table: bool,
162+
) -> Result<Statement, ParserError> {
163+
Ok(Statement::UnlockTables(pluralized_table))
151164
}

src/dialect/postgresql.rs

+52-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
// limitations under the License.
2929
use log::debug;
3030

31-
use crate::ast::{ObjectName, Statement, UserDefinedTypeRepresentation};
31+
use crate::ast::{LockTableType, LockTables, ObjectName, Statement, UserDefinedTypeRepresentation};
3232
use crate::dialect::{Dialect, Precedence};
3333
use crate::keywords::Keyword;
3434
use crate::parser::{Parser, ParserError};
@@ -139,6 +139,9 @@ impl Dialect for PostgreSqlDialect {
139139
if parser.parse_keyword(Keyword::CREATE) {
140140
parser.prev_token(); // unconsume the CREATE in case we don't end up parsing anything
141141
parse_create(parser)
142+
} else if parser.parse_keyword(Keyword::LOCK) {
143+
parser.prev_token(); // unconsume the LOCK in case we don't end up parsing anything
144+
Some(parse_lock_table(parser))
142145
} else {
143146
None
144147
}
@@ -276,3 +279,51 @@ pub fn parse_create_type_as_enum(
276279
representation: UserDefinedTypeRepresentation::Enum { labels },
277280
})
278281
}
282+
283+
pub fn parse_lock_table(parser: &mut Parser) -> Result<Statement, ParserError> {
284+
parser.expect_keyword(Keyword::LOCK)?;
285+
let has_table_keyword = parser.parse_keyword(Keyword::TABLE);
286+
let only = parser.parse_keyword(Keyword::ONLY);
287+
let tables: Vec<ObjectName> =
288+
parser.parse_comma_separated(|parser| parser.parse_object_name(false))?;
289+
let lock_mode = parse_lock_mode(parser)?;
290+
let no_wait = parser.parse_keyword(Keyword::NOWAIT);
291+
292+
Ok(Statement::LockTables(LockTables::Postgres {
293+
tables,
294+
lock_mode,
295+
has_table_keyword,
296+
only,
297+
no_wait,
298+
}))
299+
}
300+
301+
pub fn parse_lock_mode(parser: &mut Parser) -> Result<Option<LockTableType>, ParserError> {
302+
if !parser.parse_keyword(Keyword::IN) {
303+
return Ok(None);
304+
}
305+
306+
let lock_mode = if parser.parse_keywords(&[Keyword::ACCESS, Keyword::SHARE]) {
307+
LockTableType::AccessShare
308+
} else if parser.parse_keywords(&[Keyword::ACCESS, Keyword::EXCLUSIVE]) {
309+
LockTableType::AccessExclusive
310+
} else if parser.parse_keywords(&[Keyword::EXCLUSIVE]) {
311+
LockTableType::Exclusive
312+
} else if parser.parse_keywords(&[Keyword::ROW, Keyword::EXCLUSIVE]) {
313+
LockTableType::RowExclusive
314+
} else if parser.parse_keywords(&[Keyword::ROW, Keyword::SHARE]) {
315+
LockTableType::RowShare
316+
} else if parser.parse_keywords(&[Keyword::SHARE, Keyword::ROW, Keyword::EXCLUSIVE]) {
317+
LockTableType::ShareRowExclusive
318+
} else if parser.parse_keywords(&[Keyword::SHARE, Keyword::UPDATE, Keyword::EXCLUSIVE]) {
319+
LockTableType::ShareUpdateExclusive
320+
} else if parser.parse_keywords(&[Keyword::SHARE]) {
321+
LockTableType::Share
322+
} else {
323+
return Err(ParserError::ParserError("Expected: ACCESS EXCLUSIVE | ACCESS SHARE | EXCLUSIVE | ROW EXCLUSIVE | ROW SHARE | SHARE | SHARE ROW EXCLUSIVE | SHARE ROW EXCLUSIVE".into()));
324+
};
325+
326+
parser.expect_keyword(Keyword::MODE)?;
327+
328+
Ok(Some(lock_mode))
329+
}

0 commit comments

Comments
 (0)