Skip to content

Commit b4d3824

Browse files
nom-sql: Add support for generated columns for MySQL
This commit adds support for Generated Columns for MySQL. Turns out that the only required change in order to fully support generated columns in MySQL was to parse the generated columns during DDL extraction. Both snapshot and replication already bring the generated columns populated. Fixes: REA-4475 Closes: #1296 Release-Note-Core: Added support for generated columns in MySQL. Change-Id: I1be3641a7a4650480516cdfd6758178475f35200 Reviewed-on: https://gerrit.readyset.name/c/readyset/+/7751 Tested-by: Buildkite CI Reviewed-by: Michael Zink <michael.z@readyset.io>
1 parent 9606a20 commit b4d3824

File tree

15 files changed

+268
-4
lines changed

15 files changed

+268
-4
lines changed

nom-sql/src/alter.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ mod tests {
483483
table: None,
484484
},
485485
sql_type: SqlType::VarChar(Some(255)),
486+
generated: None,
486487
constraints: vec![],
487488
comment: None,
488489
}),
@@ -492,6 +493,7 @@ mod tests {
492493
table: None,
493494
},
494495
sql_type: SqlType::Text,
496+
generated: None,
495497
constraints: vec![],
496498
comment: None,
497499
}),
@@ -517,6 +519,7 @@ mod tests {
517519
table: None,
518520
},
519521
sql_type: SqlType::Int(Some(32)),
522+
generated: None,
520523
comment: None,
521524
constraints: vec![],
522525
})]),
@@ -584,6 +587,7 @@ mod tests {
584587
table: None,
585588
},
586589
sql_type: SqlType::Int(None),
590+
generated: None,
587591
constraints: vec![],
588592
comment: None,
589593
})]),
@@ -609,6 +613,7 @@ mod tests {
609613
table: None,
610614
},
611615
sql_type: SqlType::Int(None),
616+
generated: None,
612617
constraints: vec![],
613618
comment: None,
614619
}),
@@ -618,6 +623,7 @@ mod tests {
618623
table: None,
619624
},
620625
sql_type: SqlType::Text,
626+
generated: None,
621627
constraints: vec![],
622628
comment: None,
623629
}),
@@ -720,6 +726,7 @@ mod tests {
720726
spec: ColumnSpecification {
721727
column: Column::from("created_at"),
722728
sql_type: SqlType::DateTime(None),
729+
generated: None,
723730
constraints: vec![ColumnConstraint::NotNull],
724731
comment: None,
725732
}
@@ -742,6 +749,7 @@ mod tests {
742749
spec: ColumnSpecification {
743750
column: Column::from("f"),
744751
sql_type: SqlType::VarChar(Some(255)),
752+
generated: None,
745753
constraints: vec![
746754
ColumnConstraint::NotNull,
747755
ColumnConstraint::PrimaryKey
@@ -767,6 +775,7 @@ mod tests {
767775
spec: ColumnSpecification {
768776
column: Column::from("modify"),
769777
sql_type: SqlType::DateTime(None),
778+
generated: None,
770779
constraints: vec![],
771780
comment: None,
772781
}
@@ -852,6 +861,7 @@ mod tests {
852861
definitions: Ok(vec![AlterTableDefinition::AddColumn(ColumnSpecification {
853862
column: Column::from("subscription"),
854863
sql_type: SqlType::from_enum_variants(["follow".into(), "ignore".into(),]),
864+
generated: None,
855865
constraints: vec![ColumnConstraint::Null],
856866
comment: None,
857867
})]),
@@ -879,6 +889,7 @@ mod tests {
879889
table: None,
880890
},
881891
sql_type: SqlType::Int(Some(32)),
892+
generated: None,
882893
comment: None,
883894
constraints: vec![],
884895
})]),
@@ -946,6 +957,7 @@ mod tests {
946957
table: None,
947958
},
948959
sql_type: SqlType::Int(None),
960+
generated: None,
949961
constraints: vec![],
950962
comment: None,
951963
})]),
@@ -971,6 +983,7 @@ mod tests {
971983
table: None,
972984
},
973985
sql_type: SqlType::Int(None),
986+
generated: None,
974987
constraints: vec![],
975988
comment: None,
976989
}),
@@ -980,6 +993,7 @@ mod tests {
980993
table: None,
981994
},
982995
sql_type: SqlType::Text,
996+
generated: None,
983997
constraints: vec![],
984998
comment: None,
985999
}),

nom-sql/src/column.rs

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use nom::branch::alt;
55
use nom::bytes::complete::{tag, tag_no_case};
66
use nom::combinator::{map, opt};
77
use nom::multi::many0;
8-
use nom::sequence::{delimited, preceded, tuple};
8+
use nom::sequence::{delimited, preceded, terminated, tuple};
99
use nom_locate::LocatedSpan;
1010
use readyset_util::fmt::fmt_with;
1111
use serde::{Deserialize, Serialize};
@@ -128,6 +128,7 @@ impl DialectDisplay for ColumnConstraint {
128128
pub struct ColumnSpecification {
129129
pub column: Column,
130130
pub sql_type: SqlType,
131+
pub generated: Option<GeneratedColumn>,
131132
pub constraints: Vec<ColumnConstraint>,
132133
pub comment: Option<String>,
133134
}
@@ -137,6 +138,7 @@ impl ColumnSpecification {
137138
ColumnSpecification {
138139
column,
139140
sql_type,
141+
generated: None,
140142
constraints: vec![],
141143
comment: None,
142144
}
@@ -150,6 +152,7 @@ impl ColumnSpecification {
150152
ColumnSpecification {
151153
column,
152154
sql_type,
155+
generated: None,
153156
constraints,
154157
comment: None,
155158
}
@@ -292,16 +295,23 @@ pub fn column_specification(
292295
dialect: Dialect,
293296
) -> impl Fn(LocatedSpan<&[u8]>) -> NomSqlResult<&[u8], ColumnSpecification> {
294297
move |i| {
295-
let (i, (column, field_type, constraints)) = tuple((
298+
let (i, (column, field_type)) = tuple((
296299
column_identifier_no_alias(dialect),
297300
opt(delimited(
298301
whitespace1,
299302
type_identifier(dialect),
300303
whitespace0,
301304
)),
302-
many0(column_constraint(dialect)),
303305
))(i)?;
304306

307+
let (i, generated) = if matches!(dialect, Dialect::MySQL) {
308+
opt(preceded(whitespace0, generated_column(dialect)))(i)?
309+
} else {
310+
(i, None)
311+
};
312+
313+
let (i, constraints) = many0(preceded(whitespace0, column_constraint(dialect)))(i)?;
314+
305315
let (i, comment) = if matches!(dialect, Dialect::MySQL) {
306316
opt(parse_comment(dialect))(i)?
307317
} else {
@@ -318,20 +328,135 @@ pub fn column_specification(
318328
ColumnSpecification {
319329
column,
320330
sql_type,
331+
generated,
321332
constraints,
322333
comment,
323334
},
324335
))
325336
}
326337
}
327338

339+
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Arbitrary)]
340+
pub struct GeneratedColumn {
341+
pub expr: Expr,
342+
pub stored: bool,
343+
}
344+
345+
impl fmt::Display for GeneratedColumn {
346+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
347+
write!(
348+
f,
349+
"GENERATED ALWAYS AS ({}) {} ",
350+
self.expr.display(Dialect::MySQL),
351+
if self.stored { "STORED" } else { "VIRTUAL" }
352+
)
353+
}
354+
}
355+
356+
/// Parse rule for a generated column specification
357+
fn generated_column(
358+
dialect: Dialect,
359+
) -> impl Fn(LocatedSpan<&[u8]>) -> NomSqlResult<&[u8], GeneratedColumn> {
360+
move |i| {
361+
let (i, _) = opt(terminated(tag_no_case("generated always"), whitespace0))(i)?;
362+
let (i, _) = terminated(tag_no_case("as"), whitespace0)(i)?;
363+
let (i, expr) = delimited(tag("("), expression(dialect), tag(")"))(i)?;
364+
let (i, stored) = preceded(
365+
whitespace0,
366+
opt(map(
367+
alt((tag_no_case("stored"), tag_no_case("virtual"))),
368+
|i: LocatedSpan<&[u8]>| {
369+
std::str::from_utf8(i.fragment())
370+
.unwrap()
371+
.to_string()
372+
.to_lowercase()
373+
== "stored"
374+
},
375+
)),
376+
)(i)?;
377+
378+
Ok((
379+
i,
380+
GeneratedColumn {
381+
expr,
382+
stored: stored.unwrap_or(false),
383+
},
384+
))
385+
}
386+
}
387+
328388
#[cfg(test)]
329389
mod tests {
330390
use super::*;
331391

332392
mod mysql {
333393
use super::*;
334-
use crate::FunctionExpr;
394+
use crate::{BinaryOperator, FunctionExpr};
395+
396+
#[test]
397+
fn multiple_generated_column() {
398+
let mut default_gen_col = GeneratedColumn {
399+
expr: Expr::BinaryOp {
400+
lhs: Box::new(Expr::Literal(Literal::Integer(1))),
401+
op: BinaryOperator::Add,
402+
rhs: Box::new(Expr::Literal(Literal::Integer(1))),
403+
},
404+
stored: true,
405+
};
406+
407+
// Without GENERATED ALWAYS
408+
let (_, res) =
409+
generated_column(Dialect::MySQL)(LocatedSpan::new(b"AS (1 + 1) STORED")).unwrap();
410+
assert_eq!(res, default_gen_col);
411+
412+
// With GENERATED ALWAYS and STORED
413+
let (_, res) = generated_column(Dialect::MySQL)(LocatedSpan::new(
414+
b"GENERATED ALWAYS AS (1 + 1) STORED",
415+
))
416+
.unwrap();
417+
assert_eq!(res, default_gen_col);
418+
419+
// With GENERATED ALWAYS and VIRTUAL
420+
default_gen_col.stored = false;
421+
let (_, res) = generated_column(Dialect::MySQL)(LocatedSpan::new(
422+
b"GENERATED ALWAYS AS (1 + 1) VIRTUAL",
423+
))
424+
.unwrap();
425+
426+
assert_eq!(res, default_gen_col);
427+
428+
// Without STORED or VIRTUAL (defaults to VIRTUAL)
429+
let (_, res) =
430+
generated_column(Dialect::MySQL)(LocatedSpan::new(b"GENERATED ALWAYS AS (1 + 1)"))
431+
.unwrap();
432+
433+
assert_eq!(res, default_gen_col);
434+
435+
// Column specification with generated column
436+
let mut col_spec = ColumnSpecification {
437+
column: Column {
438+
name: "col1".into(),
439+
table: None,
440+
},
441+
sql_type: SqlType::Int(None),
442+
generated: Some(default_gen_col),
443+
comment: None,
444+
constraints: vec![ColumnConstraint::NotNull],
445+
};
446+
let (_, res) = column_specification(Dialect::MySQL)(LocatedSpan::new(
447+
b"`col1` INT GENERATED ALWAYS AS (1 + 1) VIRTUAL NOT NULL",
448+
))
449+
.unwrap();
450+
assert_eq!(res, col_spec);
451+
452+
// Column specification with generated column and PK
453+
col_spec.constraints.push(ColumnConstraint::PrimaryKey);
454+
let (_, res) = column_specification(Dialect::MySQL)(LocatedSpan::new(
455+
b"`col1` INT GENERATED ALWAYS AS (1 + 1) VIRTUAL NOT NULL PRIMARY KEY",
456+
))
457+
.unwrap();
458+
assert_eq!(res, col_spec);
459+
}
335460

336461
#[test]
337462
fn multiple_constraints() {
@@ -347,6 +472,7 @@ mod tests {
347472
table: None,
348473
},
349474
sql_type: SqlType::Timestamp,
475+
generated: None,
350476
comment: None,
351477
constraints: vec![
352478
ColumnConstraint::NotNull,
@@ -402,6 +528,7 @@ mod tests {
402528
table: None,
403529
},
404530
sql_type: SqlType::DateTime(Some(6)),
531+
generated: None,
405532
comment: None,
406533
constraints: vec![
407534
ColumnConstraint::NotNull,
@@ -437,6 +564,7 @@ mod tests {
437564
table: None,
438565
},
439566
sql_type: SqlType::DateTime(Some(6)),
567+
generated: None,
440568
comment: None,
441569
constraints: vec![
442570
ColumnConstraint::NotNull,
@@ -472,6 +600,7 @@ mod tests {
472600
table: None,
473601
},
474602
sql_type: SqlType::Timestamp,
603+
generated: None,
475604
comment: None,
476605
constraints: vec![
477606
ColumnConstraint::NotNull,
@@ -499,6 +628,7 @@ mod tests {
499628
table: None,
500629
},
501630
sql_type: SqlType::Timestamp,
631+
generated: None,
502632
comment: None,
503633
constraints: vec![
504634
ColumnConstraint::NotNull,

nom-sql/src/create.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,6 +1688,7 @@ mod tests {
16881688
fields: vec![ColumnSpecification {
16891689
column: "x".into(),
16901690
sql_type: SqlType::Double,
1691+
generated: None,
16911692
constraints: vec![],
16921693
comment: None,
16931694
}],
@@ -2268,6 +2269,7 @@ mod tests {
22682269
fields: vec![ColumnSpecification {
22692270
column: "x".into(),
22702271
sql_type: SqlType::Double,
2272+
generated: None,
22712273
constraints: vec![],
22722274
comment: None,
22732275
}],

query-generator/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ impl From<TableSpec> for CreateTableStatement {
380380
.map(|(col_name, col_type)| ColumnSpecification {
381381
column: col_name.into(),
382382
sql_type: col_type.sql_type,
383+
generated: None,
383384
constraints: vec![],
384385
comment: None,
385386
})

0 commit comments

Comments
 (0)