Skip to content

Commit 35b4f77

Browse files
committed
readyset-sql-parsing: Parse with both nom-sql and sqlparser-rs
This entails adding a suite of `From`/`TryFrom` impls for the AST types to convert from sqlparser's AST to ours. It is currently mostly `From` with panics and todos for easy clicking during initial implementation, but most of these should be converted to `TryFrom` later. Gated behind `sqlparser` feature flag: ```sh cargo test --features sqlparser ``` Change-Id: I28d23de30cbbd87d034270ab1572e8ca051fffb6 Reviewed-on: https://gerrit.readyset.name/c/readyset/+/8773 Reviewed-by: Jason Brown <[email protected]> Tested-by: Buildkite CI
1 parent bbc7916 commit 35b4f77

26 files changed

+2021
-26
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

readyset-adapter/src/query_status_cache.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -885,7 +885,8 @@ mod tests {
885885
fn select_statement(s: &str) -> anyhow::Result<SelectStatement> {
886886
match readyset_sql_parsing::parse_query(readyset_sql::Dialect::MySQL, s) {
887887
Ok(SqlQuery::Select(s)) => Ok(s),
888-
_ => Err(anyhow::anyhow!("Invalid SELECT statement")),
888+
Ok(q) => Err(anyhow::anyhow!("Not a SELECT statement: {q:?}")),
889+
Err(e) => Err(anyhow::anyhow!("Parsing error: {e}")),
889890
}
890891
}
891892

readyset-adapter/src/utils.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -917,10 +917,10 @@ mod tests {
917917

918918
#[test]
919919
fn test_dollar_number_parameter_column_extraction() {
920-
let query = "SELECT `votes`.* FROM `votes` WHERE `votes`.`user_id` = 1 \
921-
AND `votes`.`story_id` = $1 AND `votes`.`comment_id` IS NULL \
922-
ORDER BY `votes`.`id` ASC LIMIT 1";
923-
let q = readyset_sql_parsing::parse_query(Dialect::MySQL, query).unwrap();
920+
let query = r#"SELECT "votes".* FROM "votes" WHERE "votes"."user_id" = 1
921+
AND "votes"."story_id" = $1 AND "votes"."comment_id" IS NULL
922+
ORDER BY "votes"."id" ASC LIMIT 1"#;
923+
let q = readyset_sql_parsing::parse_query(Dialect::PostgreSQL, query).unwrap();
924924

925925
let pc = get_parameter_columns(&q);
926926

readyset-e2e-tests/tests/mysql_long_data.rs

-4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ async fn mysql_send_long_data_inner() {
2929
.query_drop("CREATE TABLE t (x LONGBLOB)")
3030
.await
3131
.unwrap();
32-
rs_conn
33-
.query_drop("SET SESSION max_execution_time=60000")
34-
.await
35-
.unwrap();
3632
rs_conn
3733
.exec_drop(
3834
"INSERT INTO t VALUES (?)",

readyset-mysql/tests/integration.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ async fn duplicate_join_key() {
8484
.await
8585
.unwrap();
8686

87-
conn.query_drop("SELECT a.id FROM a JOIN b on b.a_id = a.id WHERE a.id = -1")
87+
conn.query_drop("SELECT a.id FROM a JOIN b on b.a_id = a.id WHERE a.id = 42")
8888
.await
8989
.unwrap();
9090

readyset-sql-parsing/Cargo.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ edition = "2021"
77

88
[dependencies]
99
nom-sql = { path = "../nom-sql" }
10+
pretty_assertions = { workspace = true }
1011
readyset-sql = { path = "../readyset-sql" }
11-
sqlparser = { workspace = true }
12+
sqlparser = { workspace = true, optional = true }
1213
thiserror = { workspace = true }
14+
tracing = { workspace = true }
15+
16+
[features]
17+
sqlparser = ["dep:sqlparser"]

readyset-sql-parsing/src/lib.rs

+51
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,56 @@
11
use readyset_sql::{ast::SqlQuery, Dialect};
22

3+
#[cfg(feature = "sqlparser")]
4+
fn sqlparser_dialect_from_readyset_dialect(
5+
dialect: Dialect,
6+
) -> Box<dyn sqlparser::dialect::Dialect> {
7+
match dialect {
8+
Dialect::PostgreSQL => Box::new(sqlparser::dialect::PostgreSqlDialect {}),
9+
Dialect::MySQL => Box::new(sqlparser::dialect::MySqlDialect {}),
10+
}
11+
}
12+
13+
#[cfg(feature = "sqlparser")]
14+
pub fn parse_query(dialect: Dialect, input: impl AsRef<str>) -> Result<SqlQuery, String> {
15+
let nom_result = nom_sql::parse_query(dialect, input.as_ref());
16+
let sqlparser_dialect = sqlparser_dialect_from_readyset_dialect(dialect);
17+
let sqlparser_result: Result<SqlQuery, _> =
18+
sqlparser::parser::Parser::new(sqlparser_dialect.as_ref())
19+
.try_with_sql(input.as_ref())
20+
.and_then(|mut p| p.parse_statement())
21+
.map_err(|e| format!("failed to parse: {e}"))
22+
.and_then(|q| {
23+
q.try_into()
24+
.map_err(|e| format!("failed to convert AST: {e}"))
25+
});
26+
match (&nom_result, sqlparser_result) {
27+
(Ok(nom_ast), Ok(sqlparser_ast)) => {
28+
pretty_assertions::assert_eq!(
29+
nom_ast,
30+
&sqlparser_ast,
31+
"nom-sql AST differs from sqlparser-rs AST. input: {:?}",
32+
input.as_ref()
33+
);
34+
}
35+
(Ok(nom_ast), Err(sqlparser_error)) => {
36+
panic!(
37+
"nom-sql succeeded but sqlparser-rs failed: {}\ninput: {}\nnom_ast: {:?}",
38+
sqlparser_error,
39+
input.as_ref(),
40+
nom_ast
41+
)
42+
}
43+
(Err(nom_error), Ok(sqlparser_ast)) => {
44+
tracing::warn!(%nom_error, ?sqlparser_ast, "sqlparser-rs succeeded but nom-sql failed")
45+
}
46+
(Err(nom_error), Err(sqlparser_error)) => {
47+
tracing::warn!(%nom_error, %sqlparser_error, "both nom-sql and sqlparser-rs failed");
48+
}
49+
}
50+
nom_result
51+
}
52+
53+
#[cfg(not(feature = "sqlparser"))]
354
pub fn parse_query(dialect: Dialect, input: impl AsRef<str>) -> Result<SqlQuery, String> {
455
nom_sql::parse_query(dialect, input.as_ref())
556
}

readyset-sql/src/ast/column.rs

+160-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use readyset_util::fmt::fmt_with;
44
use serde::{Deserialize, Serialize};
55
use test_strategy::Arbitrary;
66

7-
use crate::{ast::*, Dialect, DialectDisplay};
7+
use crate::{ast::*, AstConversionError, Dialect, DialectDisplay};
88

99
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Arbitrary)]
1010
pub struct Column {
@@ -33,6 +33,66 @@ impl From<&'_ str> for Column {
3333
}
3434
}
3535

36+
impl From<sqlparser::ast::Ident> for Column {
37+
fn from(value: sqlparser::ast::Ident) -> Self {
38+
Self {
39+
name: value.into(),
40+
table: None,
41+
}
42+
}
43+
}
44+
45+
impl From<Vec<sqlparser::ast::Ident>> for Column {
46+
fn from(mut value: Vec<sqlparser::ast::Ident>) -> Self {
47+
let name: SqlIdentifier = value.pop().unwrap().into();
48+
let table = if let Some(table) = value.pop() {
49+
if let Some(schema) = value.pop() {
50+
Some(Relation {
51+
schema: Some(schema.into()),
52+
name: table.into(),
53+
})
54+
} else {
55+
Some(Relation {
56+
schema: None,
57+
name: table.into(),
58+
})
59+
}
60+
} else {
61+
None
62+
};
63+
Self { name, table }
64+
}
65+
}
66+
67+
impl From<sqlparser::ast::ObjectName> for Column {
68+
fn from(value: sqlparser::ast::ObjectName) -> Self {
69+
value
70+
.0
71+
.into_iter()
72+
.map(|sqlparser::ast::ObjectNamePart::Identifier(ident)| ident)
73+
.collect::<Vec<_>>()
74+
.into()
75+
}
76+
}
77+
78+
impl From<sqlparser::ast::ViewColumnDef> for Column {
79+
fn from(value: sqlparser::ast::ViewColumnDef) -> Self {
80+
Self {
81+
name: value.name.into(),
82+
table: None,
83+
}
84+
}
85+
}
86+
87+
impl From<sqlparser::ast::AssignmentTarget> for Column {
88+
fn from(value: sqlparser::ast::AssignmentTarget) -> Self {
89+
match value {
90+
sqlparser::ast::AssignmentTarget::ColumnName(object_name) => object_name.into(),
91+
sqlparser::ast::AssignmentTarget::Tuple(_vec) => todo!("tuple assignment syntax"),
92+
}
93+
}
94+
}
95+
3696
impl Ord for Column {
3797
fn cmp(&self, other: &Column) -> Ordering {
3898
match (self.table.as_ref(), other.table.as_ref()) {
@@ -120,6 +180,105 @@ pub struct ColumnSpecification {
120180
pub comment: Option<String>,
121181
}
122182

183+
impl TryFrom<sqlparser::ast::ColumnDef> for ColumnSpecification {
184+
type Error = AstConversionError;
185+
186+
fn try_from(value: sqlparser::ast::ColumnDef) -> Result<Self, Self::Error> {
187+
use sqlparser::{
188+
keywords::Keyword,
189+
tokenizer::{Token, Word},
190+
};
191+
192+
let mut comment = None;
193+
let mut constraints = vec![];
194+
let mut generated = None;
195+
for option in value.options {
196+
match option.option {
197+
sqlparser::ast::ColumnOption::Null => constraints.push(ColumnConstraint::Null),
198+
sqlparser::ast::ColumnOption::NotNull => {
199+
constraints.push(ColumnConstraint::NotNull)
200+
}
201+
sqlparser::ast::ColumnOption::Default(expr) => {
202+
constraints.push(ColumnConstraint::DefaultValue(expr.try_into()?))
203+
}
204+
sqlparser::ast::ColumnOption::Unique {
205+
is_primary,
206+
characteristics,
207+
} => {
208+
if characteristics.is_some() {
209+
return not_yet_implemented!("constraint timing on column definitions");
210+
} else if is_primary {
211+
constraints.push(ColumnConstraint::PrimaryKey)
212+
} else {
213+
constraints.push(ColumnConstraint::Unique)
214+
}
215+
}
216+
sqlparser::ast::ColumnOption::ForeignKey { .. } => {
217+
return not_yet_implemented!("foreign key");
218+
}
219+
sqlparser::ast::ColumnOption::DialectSpecific(vec) => {
220+
if vec.iter().any(|token| {
221+
matches!(
222+
token,
223+
Token::Word(Word {
224+
keyword: Keyword::AUTO_INCREMENT,
225+
..
226+
})
227+
)
228+
}) {
229+
constraints.push(ColumnConstraint::AutoIncrement)
230+
}
231+
}
232+
sqlparser::ast::ColumnOption::CharacterSet(object_name) => {
233+
constraints.push(ColumnConstraint::CharacterSet(object_name.to_string()))
234+
}
235+
sqlparser::ast::ColumnOption::Collation(object_name) => {
236+
constraints.push(ColumnConstraint::Collation(object_name.to_string()))
237+
}
238+
sqlparser::ast::ColumnOption::Comment(s) => {
239+
comment = Some(s);
240+
}
241+
sqlparser::ast::ColumnOption::OnUpdate(_expr) => {
242+
todo!("on update (check for current_timestamp)")
243+
}
244+
sqlparser::ast::ColumnOption::Generated {
245+
generated_as: _,
246+
sequence_options: _,
247+
generation_expr,
248+
generation_expr_mode,
249+
generated_keyword: _,
250+
} => {
251+
generated = Some(GeneratedColumn {
252+
expr: generation_expr
253+
.map(TryInto::try_into)
254+
.expect("generated expr can't be None")?,
255+
stored: generation_expr_mode
256+
== Some(sqlparser::ast::GeneratedExpressionMode::Stored),
257+
})
258+
}
259+
sqlparser::ast::ColumnOption::Materialized(_)
260+
| sqlparser::ast::ColumnOption::Ephemeral(_)
261+
| sqlparser::ast::ColumnOption::Alias(_)
262+
| sqlparser::ast::ColumnOption::Check(_)
263+
| sqlparser::ast::ColumnOption::Options(_)
264+
| sqlparser::ast::ColumnOption::Identity(_)
265+
| sqlparser::ast::ColumnOption::OnConflict(_)
266+
| sqlparser::ast::ColumnOption::Policy(_)
267+
| sqlparser::ast::ColumnOption::Tags(_) => {
268+
// Don't care about these options
269+
}
270+
}
271+
}
272+
Ok(Self {
273+
column: value.name.into(),
274+
sql_type: value.data_type.try_into()?,
275+
constraints,
276+
comment,
277+
generated,
278+
})
279+
}
280+
}
281+
123282
impl ColumnSpecification {
124283
pub fn new(column: Column, sql_type: SqlType) -> ColumnSpecification {
125284
ColumnSpecification {

0 commit comments

Comments
 (0)