Skip to content

Add Postgres-specific statements PREPARE, EXECUTE and DEALLOCATE #243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,12 +551,34 @@ pub enum Statement {
Rollback { chain: bool },
/// CREATE SCHEMA
CreateSchema { schema_name: ObjectName },

/// ASSERT <condition> [AS <message>]
Assert {
condition: Expr,
message: Option<Expr>,
},
/// DEALLOCATE [ PREPARE ] { name | ALL }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be enclosed in backticks (as well as the ASSERT comment above -- sorry about missing that!), so that cargo doc output is properly formatted. Same below.

///
/// Note: this is a PostgreSQL-specific statement.
Deallocate {
name: Option<ObjectName>,
all: bool,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need to handle ALL separately, from the parser viewpoint, it's as good an object name, as any. Can name be schema-qualified? The docs are not clear on that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. I tried to check and test again. It is not schema-qualified. I will remove all and fix type of name to an Ident.

prepare: bool,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be meaningless syntax. It's fine to keep it, but maybe note that PostgreSQL docs say it's ignored?

Copy link
Contributor Author

@silathdiir silathdiir Jul 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The syntax is meaningless. But I think if we should output (format) the useless keyword PREPARE as the SQL passed into. What's your opinion?

},
/// EXECUTE name [ ( parameter [, ...] ) ]
///
/// Note: this is a PostgreSQL-specific statement.
Execute {
name: ObjectName,
parameters: Vec<Expr>,
},
/// PREPARE name [ ( data_type [, ...] ) ] AS statement
///
/// Note: this is a PostgreSQL-specific statement.
Prepare {
name: ObjectName,
data_types: Vec<DataType>,
statement: Box<Statement>,
},
}

impl fmt::Display for Statement {
Expand Down Expand Up @@ -824,6 +846,36 @@ impl fmt::Display for Statement {
}
Ok(())
}
Statement::Deallocate { name, all, prepare } => {
f.write_str("DEALLOCATE ")?;
if *prepare {
f.write_str("PREPARE ")?;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we're inconsistent about that, but I believe using the same style as in the Statement::CreateTable branch will yield smaller code (especially we figure out the ALL vs object name question above).

if *all {
f.write_str("ALL")
} else {
write!(f, "{}", name.as_ref().unwrap())
}
}
Statement::Execute { name, parameters } => {
write!(f, "EXECUTE {}", name)?;
if !parameters.is_empty() {
write!(f, "({})", display_comma_separated(parameters))?;
}
Ok(())
}
Statement::Prepare {
name,
data_types,
statement,
} => {
write!(f, "PREPARE {} ", name)?;
if !data_types.is_empty() {
write!(f, "({}) ", display_comma_separated(data_types))?;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I know we're inconsistent about this, but it seems that we usually don't add blank lines in situations like this.

write!(f, "AS {}", statement)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ impl Dialect for PostgreSqlDialect {
// See https://www.postgresql.org/docs/11/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
// We don't yet support identifiers beginning with "letters with
// diacritical marks and non-Latin letters"
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '$'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't match the documentation linked just above this line (Postgres seems to have $1 and $$comments$$, but I don't see how $foo is supposed to be parsed), so I'd like to split this separately. You should be able to keep it in a dialect of your own for now.

}

fn is_identifier_part(&self, ch: char) -> bool {
Expand Down
48 changes: 48 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ impl Parser {
Keyword::COMMIT => Ok(self.parse_commit()?),
Keyword::ROLLBACK => Ok(self.parse_rollback()?),
Keyword::ASSERT => Ok(self.parse_assert()?),
// `PREPARE`, `EXECUTE` and `DEALLOCATE` are Postgres-specific
// syntaxes. They are used for Postgres prepared statement.
Keyword::DEALLOCATE => Ok(self.parse_deallocate()?),
Keyword::EXECUTE => Ok(self.parse_execute()?),
Keyword::PREPARE => Ok(self.parse_prepare()?),
_ => self.expected("an SQL statement", Token::Word(w)),
},
Token::LParen => {
Expand Down Expand Up @@ -2386,6 +2391,49 @@ impl Parser {
Ok(false)
}
}

fn parse_deallocate(&mut self) -> Result<Statement, ParserError> {
let prepare = self.parse_keyword(Keyword::PREPARE);
let all = self.parse_keyword(Keyword::ALL);

let name = if all {
None
} else {
Some(self.parse_object_name()?)
};

Ok(Statement::Deallocate { name, all, prepare })
}

fn parse_execute(&mut self) -> Result<Statement, ParserError> {
let name = self.parse_object_name()?;

let mut parameters = vec![];
if self.expect_token(&Token::LParen).is_ok() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have confusingly named consume_token for that. Same below.

parameters = self.parse_comma_separated(Parser::parse_expr)?;
self.expect_token(&Token::RParen)?;
}

Ok(Statement::Execute { name, parameters })
}

fn parse_prepare(&mut self) -> Result<Statement, ParserError> {
let name = self.parse_object_name()?;

let mut data_types = vec![];
if self.expect_token(&Token::LParen).is_ok() {
data_types = self.parse_comma_separated(Parser::parse_data_type)?;
self.expect_token(&Token::RParen)?;
}

self.expect_keyword(Keyword::AS)?;
let statement = Box::new(self.parse_statement()?);
Ok(Statement::Prepare {
name,
data_types,
statement,
})
}
}

impl Word {
Expand Down
133 changes: 133 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,139 @@ fn parse_show() {
)
}

#[test]
fn parse_deallocate() {
let stmt = pg().verified_stmt("DEALLOCATE a");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use pg_and_generic() please.

assert_eq!(
stmt,
Statement::Deallocate {
name: Some(ObjectName(vec!["a".into()])),
all: false,
prepare: false,
}
);

let stmt = pg().verified_stmt("DEALLOCATE ALL");
assert_eq!(
stmt,
Statement::Deallocate {
name: None,
all: true,
prepare: false,
}
);

let stmt = pg().verified_stmt("DEALLOCATE PREPARE a");
assert_eq!(
stmt,
Statement::Deallocate {
name: Some(ObjectName(vec!["a".into()])),
all: false,
prepare: true,
}
);

let stmt = pg().verified_stmt("DEALLOCATE PREPARE ALL");
assert_eq!(
stmt,
Statement::Deallocate {
name: None,
all: true,
prepare: true,
}
);
}

#[test]
fn parse_execute() {
let stmt = pg().verified_stmt("EXECUTE a");
assert_eq!(
stmt,
Statement::Execute {
name: ObjectName(vec!["a".into()]),
parameters: vec![],
}
);

let stmt = pg().verified_stmt("EXECUTE a(1, 't')");

#[cfg(feature = "bigdecimal")]
assert_eq!(
stmt,
Statement::Execute {
name: ObjectName(vec!["a".into()]),
parameters: vec![
Expr::Value(Value::Number(bigdecimal::BigDecimal::from(1))),
Expr::Value(Value::SingleQuotedString("t".to_string()))
],
}
);
}

#[test]
fn parse_prepare() {
let stmt = pg().verified_stmt("PREPARE a AS INSERT INTO customers VALUES ($1, $2, $3)");
let sub_stmt = match stmt {
Statement::Prepare {
name,
data_types,
statement,
..
} => {
assert_eq!(name, ObjectName(vec!["a".into()]));
assert!(data_types.is_empty());

statement
}
_ => unreachable!(),
};
match sub_stmt.as_ref() {
Statement::Insert {
table_name,
columns,
source,
..
} => {
assert_eq!(table_name.to_string(), "customers");
assert!(columns.is_empty());

let expected_values = [vec![
Expr::Identifier("$1".into()),
Expr::Identifier("$2".into()),
Expr::Identifier("$3".into()),
]];
match &source.body {
SetExpr::Values(Values(values)) => assert_eq!(values.as_slice(), &expected_values),
_ => unreachable!(),
}
}
_ => unreachable!(),
};

let stmt = pg()
.verified_stmt("PREPARE a (INT, TEXT) AS SELECT * FROM customers WHERE customers.id = $1");
let sub_stmt = match stmt {
Statement::Prepare {
name,
data_types,
statement,
..
} => {
assert_eq!(name, ObjectName(vec!["a".into()]));
assert_eq!(data_types, vec![DataType::Int, DataType::Text]);

statement
}
_ => unreachable!(),
};
assert_eq!(
sub_stmt,
Box::new(Statement::Query(Box::new(pg().verified_query(
"SELECT * FROM customers WHERE customers.id = $1"
))))
);
}

fn pg() -> TestedDialects {
TestedDialects {
dialects: vec![Box::new(PostgreSqlDialect {})],
Expand Down