Skip to content
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

Parse MySQL column default value #110

Merged
merged 18 commits into from
Jun 10, 2023
10 changes: 7 additions & 3 deletions src/mysql/def/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ pub enum ColumnKey {

#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
pub struct ColumnDefault {
/// default value expression
pub expr: String,
pub enum ColumnDefault {
Null,
Int(i64),
Real(f64),
String(String),
CustomExpr(String),
CurrentTimestamp,
}

#[derive(Clone, Debug, PartialEq)]
Expand Down
5 changes: 3 additions & 2 deletions src/mysql/discovery/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ impl SchemaDiscovery {
pub async fn discover_table(&self, info: TableInfo) -> Result<TableDef, SqlxError> {
let table = SeaRc::new(Alias::new(info.name.as_str()));
let columns = self
.discover_columns(self.schema.clone(), table.clone())
.discover_columns(self.schema.clone(), table.clone(), &self.query.system)
.await?;
let indexes = self
.discover_indexes(self.schema.clone(), table.clone())
Expand All @@ -114,6 +114,7 @@ impl SchemaDiscovery {
&self,
schema: SeaRc<dyn Iden>,
table: SeaRc<dyn Iden>,
system: &SystemInfo,
) -> Result<Vec<ColumnInfo>, SqlxError> {
let rows = self
.executor
Expand All @@ -125,7 +126,7 @@ impl SchemaDiscovery {
.map(|row| {
let result: ColumnQueryResult = row.into();
debug_print!("{:?}", result);
let column = result.parse();
let column = result.parse(system);
debug_print!("{:?}", column);
column
})
Expand Down
76 changes: 68 additions & 8 deletions src/mysql/parser/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ use crate::{parser::Parser, Name};
use sea_query::{EscapeBuilder, MysqlQueryBuilder};

impl ColumnQueryResult {
pub fn parse(self) -> ColumnInfo {
parse_column_query_result(self)
pub fn parse(self, system: &SystemInfo) -> ColumnInfo {
parse_column_query_result(self, system)
}
}

pub fn parse_column_query_result(result: ColumnQueryResult) -> ColumnInfo {
pub fn parse_column_query_result(result: ColumnQueryResult, system: &SystemInfo) -> ColumnInfo {
let col_type = parse_column_type(&mut Parser::new(&result.column_type));
let default = parse_column_default(&col_type, result.column_default, &result.extra, system);
ColumnInfo {
name: result.column_name,
col_type: parse_column_type(&mut Parser::new(&result.column_type)),
col_type,
null: parse_column_null(&result.is_nullable),
key: parse_column_key(&result.column_key),
default: parse_column_default(result.column_default),
default,
extra: parse_column_extra(&mut Parser::new(&result.extra)),
expression: match result.generation_expression {
Some(generation_expression) => parse_generation_expression(generation_expression),
Expand Down Expand Up @@ -260,11 +262,23 @@ pub fn parse_column_key(string: &str) -> ColumnKey {
}
}

pub fn parse_column_default(column_default: Option<String>) -> Option<ColumnDefault> {
match column_default {
pub fn parse_column_default(
col_type: &Type,
default: Option<String>,
extra: &str,
system: &SystemInfo,
) -> Option<ColumnDefault> {
match default {
Some(default) => {
if !default.is_empty() {
Copy link
Member

@tyt2y3 tyt2y3 Jun 8, 2023

Choose a reason for hiding this comment

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

I just read https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html again,
I think the logic should be as follows:

  1. parse the EXTRA column; if it contains DEFAULT_GENERATED, then the COLUMN_DEFAULT should be regarded as an expression
  2. if the default is an expression, parse it as a Expr and recognize the keywords CURRENT_TIMESTAMP, otherwise Custom
  3. if the default is not an expression, then it's a literal; then we should
    a. if it is numeric-like then regard it as Number
    b. otherwise regard it as String

image
image

1. if matches a keyword (CURRENT_TIMESTAMP) then make it a corresponding keyword
  1. if it starts with a ( regard it as an expression
  2. if it is numeric-like then regard it as Number. not sure whether it's worth the effort to classify into int and real/decimal
  3. finally, consider it a String

It's really a legacy that MySQL does not quote a literal string. It's given as hello instead of 'hello'.

Edit: I was wrong; apparently COLUMNS table does not give a () for expressions either.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we cannot match the ( character. The expression (CURRENT_TIMESTAMP) or CURRENT_TIMESTAMP both are valid syntax to define default value on table creation. But when you query the default expression via information schema, you get different result.

image

Some(ColumnDefault { expr: default })
let default_value = if system.is_mysql() && system.version >= 80000 {
parse_mysql_8_default(default, extra)
} else if system.is_maria_db() && system.version >= 100207 {
parse_mariadb_10_default(default)
} else {
parse_mysql_5_default(default, col_type)
};
Some(default_value)
} else {
None
}
Expand All @@ -273,6 +287,52 @@ pub fn parse_column_default(column_default: Option<String>) -> Option<ColumnDefa
}
}

pub fn parse_mysql_5_default(default: String, col_type: &Type) -> ColumnDefault {
let is_date_time = matches!(col_type, Type::DateTime(_) | Type::Timestamp(_));
if is_date_time && default == "CURRENT_TIMESTAMP" {
ColumnDefault::CurrentTimestamp
} else if let Ok(int) = default.parse() {
ColumnDefault::Int(int)
} else if let Ok(real) = default.parse() {
ColumnDefault::Real(real)
} else {
ColumnDefault::String(default)
}
}

pub fn parse_mysql_8_default(default: String, extra: &str) -> ColumnDefault {
let is_expression = extra.contains("DEFAULT_GENERATED");
if is_expression && default == "CURRENT_TIMESTAMP" {
ColumnDefault::CurrentTimestamp
} else if is_expression && default == "NULL" {
ColumnDefault::Null
} else if let Ok(int) = default.parse() {
ColumnDefault::Int(int)
} else if let Ok(real) = default.parse() {
ColumnDefault::Real(real)
} else if is_expression {
ColumnDefault::CustomExpr(default)
} else {
ColumnDefault::String(default)
Copy link
Member

Choose a reason for hiding this comment

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

in this case the tail case should be split:

  1. is_expression = true -> ColumnDefault::CustomExpr
  2. is_expression = false -> ColumnDefault::String

Copy link
Member Author

Choose a reason for hiding this comment

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

}
}

pub fn parse_mariadb_10_default(default: String) -> ColumnDefault {
if default.starts_with('\'') && default.ends_with('\'') {
ColumnDefault::String(default[1..(default.len() - 1)].into())
} else if let Ok(int) = default.parse() {
ColumnDefault::Int(int)
} else if let Ok(real) = default.parse() {
ColumnDefault::Real(real)
} else if default == "current_timestamp()" {
ColumnDefault::CurrentTimestamp
} else if default == "NULL" {
ColumnDefault::Null
} else {
ColumnDefault::CustomExpr(default)
}
}

pub fn parse_generation_expression(string: String) -> Option<ColumnExpression> {
if string.is_empty() {
None
Expand Down
19 changes: 13 additions & 6 deletions src/mysql/writer/column.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::mysql::def::{CharSet, ColumnInfo, NumericAttr, StringAttr, Type};
use crate::mysql::def::{CharSet, ColumnDefault, ColumnInfo, NumericAttr, StringAttr, Type};
use sea_query::{
Alias, BlobSize, ColumnDef, DynIden, EscapeBuilder, Iden, IntoIden, MysqlQueryBuilder,
Alias, BlobSize, ColumnDef, DynIden, EscapeBuilder, Expr, Iden, IntoIden, Keyword,
MysqlQueryBuilder, SimpleExpr,
};
use std::fmt::Write;

Expand All @@ -15,10 +16,16 @@ impl ColumnInfo {
col_def.auto_increment();
}
let mut extras = Vec::new();
if let Some(default) = self.default.as_ref() {
let mut string = "".to_owned();
write!(&mut string, "DEFAULT {}", default.expr).unwrap();
extras.push(string);
if let Some(default) = &self.default {
let default_expr: SimpleExpr = match default {
ColumnDefault::Null => Option::<bool>::None.into(),
ColumnDefault::Int(int) => (*int).into(),
ColumnDefault::Real(double) => (*double).into(),
ColumnDefault::String(string) => string.into(),
ColumnDefault::CustomExpr(string) => Expr::cust(string),
ColumnDefault::CurrentTimestamp => Keyword::CurrentTimestamp.into(),
};
col_def.default(default_expr);
}
if self.extra.on_update_current_timestamp {
extras.push("ON UPDATE CURRENT_TIMESTAMP".to_owned());
Expand Down
8 changes: 2 additions & 6 deletions src/mysql/writer/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,7 @@ mod tests {
null: false,
key: ColumnKey::NotKey,
default: Some(
ColumnDefault {
expr: "CURRENT_TIMESTAMP".to_owned(),
},
ColumnDefault::CurrentTimestamp,
),
extra: ColumnExtra {
auto_increment: false,
Expand Down Expand Up @@ -219,9 +217,7 @@ mod tests {
null: false,
key: ColumnKey::NotKey,
default: Some(
ColumnDefault {
expr: "CURRENT_TIMESTAMP".to_owned(),
},
ColumnDefault::CurrentTimestamp,
),
extra: ColumnExtra {
auto_increment: false,
Expand Down
23 changes: 21 additions & 2 deletions tests/live/mysql/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use pretty_assertions::assert_eq;
use regex::Regex;
use sea_schema::mysql::{def::TableDef, discovery::SchemaDiscovery};
use sea_schema::sea_query::{
Alias, ColumnDef, ForeignKey, ForeignKeyAction, Index, MysqlQueryBuilder, Table,
Alias, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Index, MysqlQueryBuilder, Table,
TableCreateStatement, TableRef,
};
use sqlx::{MySql, MySqlPool, Pool};
Expand Down Expand Up @@ -206,7 +206,26 @@ fn create_order_table() -> TableCreateStatement {
.col(
ColumnDef::new(Alias::new("placed_at"))
.date_time()
.not_null(),
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Alias::new("updated"))
.date_time()
.not_null()
.default("2023-06-07 16:24:00"),
)
.col(
ColumnDef::new(Alias::new("net_weight"))
.double()
.not_null()
.default(10.05),
)
.col(
ColumnDef::new(Alias::new("priority"))
.integer()
.not_null()
.default(5),
)
.index(
Index::create()
Expand Down