Skip to content

Commit

Permalink
Merge pull request #1 from urkle/feat-improve-spec-compliance
Browse files Browse the repository at this point in the history
feat: improve spec compliance
  • Loading branch information
apskhem authored Jan 3, 2025
2 parents 2876f5e + e1ba154 commit 7d3b0db
Show file tree
Hide file tree
Showing 32 changed files with 11,273 additions and 520 deletions.
2 changes: 2 additions & 0 deletions src/ast/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub enum TopLevelBlock {
Table(TableBlock),
/// Represents a table group block in the DBML file.
TableGroup(TableGroupBlock),
/// Represents a note block in the DBML file.
Note(NoteBlock),
/// Represents a reference block in the DBML file.
Ref(RefBlock),
/// Represents an enum block in the DBML file.
Expand Down
14 changes: 14 additions & 0 deletions src/ast/table_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,22 @@ pub struct TableGroupBlock {
pub span_range: SpanRange,
/// The name of a table group
pub ident: Ident,
/// The note block associated with the table group block.
pub note: Option<NoteBlock>,
/// The list of table identifiers inside the group.
pub items: Vec<TableGroupItem>,

/// The settings for the table group.
pub settings: Option<TableGroupSettings>,
}

/// Represents settings of the table group.
#[derive(Debug, Clone, Default)]
pub struct TableGroupSettings {
/// The range of the span in the source text.
pub span_range: SpanRange,
/// A vector of key and optional value pairs representing attributes of the table.
pub attributes: Vec<Attribute>,
}

/// Represents an associated table identifier listed inside a table group.
Expand Down
17 changes: 7 additions & 10 deletions src/dbml.pest
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
WHITESPACE = _{ " " | "\t" | NEWLINE }
COMMENT = _{ single_line_comment | multi_line_comment }

// whitespaces and comments
wsc = _{ WHITESPACE | COMMENT }

// comments
single_line_comment = { "//" ~ (!NEWLINE ~ ANY)* }
multi_line_comment = { "/*" ~ (!"*/" ~ ANY)* ~ "*/" }
Expand All @@ -25,7 +22,7 @@ ident = { var | double_quoted_string }

// attributes and properties
property = { spaced_var ~ ":" ~ (value | spaced_var) }
attribute = { spaced_var ~ (":" ~ (value | spaced_var))? }
attribute = { spaced_var ~ (":" ~ (value | spaced_var | double_quoted_string))? }
block_settings = { "[" ~ (attribute ~ ("," ~ attribute)*)? ~ "]" }

// values
Expand Down Expand Up @@ -76,20 +73,20 @@ col_attribute = { ref_inline | attribute }
col_settings = { "[" ~ (col_attribute ~ ("," ~ col_attribute)*)? ~ "]" }
col_type_arg = { "(" ~ (value ~ ("," ~ value)*)? ~ ")" }
col_type_array = { "[" ~ integer? ~ "]" }
col_type_unquoted = { var ~ col_type_arg? }
col_type_quoted = { "\"" ~ spaced_var ~ col_type_arg? ~ col_type_array* ~ "\"" }
col_type = { col_type_unquoted | col_type_quoted }
col_type_unquoted = ${ var ~ col_type_arg? ~ col_type_array* }
col_type_quoted = ${ "\"" ~ spaced_var ~ col_type_arg? ~ col_type_array* ~ "\"" ~ col_type_array* }
col_type = ${ (ident ~ ".")? ~ (col_type_quoted | col_type_unquoted ) }
table_col = { ident ~ col_type ~ col_settings? }
table_block = { "{" ~ (table_col | note_decl | indexes_decl)* ~ "}" }
table_alias = { ^"as " ~ ident }
table_decl = { ^"Table " ~ decl_ident ~ table_alias? ~ block_settings? ~ table_block }

// table group block
table_group_block = ${ "{" ~ wsc* ~ (decl_ident ~ wsc*)* ~ "}" }
table_group_decl = { ^"TableGroup " ~ ident ~ table_group_block }
table_group_block = { "{" ~ (note_decl | decl_ident)* ~ "}" }
table_group_decl = { ^"TableGroup " ~ ident ~ block_settings? ~ table_group_block }

// project block
project_block = { "{" ~ (note_decl | property)* ~ "}" }
project_decl = { ^"Project " ~ ident ~ project_block }

schema = { SOI ~ (project_decl | table_decl | enum_decl | ref_decl | table_group_decl)* ~ EOI }
schema = { SOI ~ (project_decl | table_decl | enum_decl | ref_decl | table_group_decl | note_decl)* ~ EOI }
80 changes: 59 additions & 21 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ fn parse_schema<'a>(pair: Pair<Rule>, input: &'a str) -> ParserResult<SchemaBloc
Rule::table_decl => acc.blocks.push(TopLevelBlock::Table(parse_table_decl(p1)?)),
Rule::enum_decl => acc.blocks.push(TopLevelBlock::Enum(parse_enum_decl(p1)?)),
Rule::ref_decl => acc.blocks.push(TopLevelBlock::Ref(parse_ref_decl(p1)?)),
Rule::note_decl => acc.blocks.push(TopLevelBlock::Note(parse_note_decl(p1)?)),
Rule::table_group_decl => acc.blocks.push(TopLevelBlock::TableGroup(parse_table_group_decl(p1)?)),
Rule::EOI => (),
_ => {
Expand Down Expand Up @@ -174,7 +175,7 @@ fn parse_table_decl(pair: Pair<Rule>) -> ParserResult<TableBlock> {
}
}
Rule::block_settings => {
acc.settings = Some(parse_block_settings(p1)?);
acc.settings = Some(parse_table_settings(p1)?);
}
_ => {
throw_rules(
Expand All @@ -193,23 +194,19 @@ fn parse_table_decl(pair: Pair<Rule>) -> ParserResult<TableBlock> {
})
}

fn parse_block_settings(pair: Pair<Rule>) -> ParserResult<TableSettings> {
let mut init = TableSettings {
fn parse_table_settings(pair: Pair<Rule>) -> ParserResult<TableSettings> {
Ok(TableSettings {
span_range: s2r(pair.as_span()),
..Default::default()
};

init.attributes = pair
.into_inner()
.map(|p1| {
match p1.as_rule() {
Rule::attribute => parse_attribute(p1),
_ => throw_rules(&[Rule::attribute], p1),
}
})
.collect::<ParserResult<_>>()?;

Ok(init)
attributes: pair
.into_inner()
.map(|p1| {
match p1.as_rule() {
Rule::attribute => parse_attribute(p1),
_ => throw_rules(&[Rule::attribute], p1),
}
})
.collect::<ParserResult<_>>()?,
})
}

fn parse_table_col(pair: Pair<Rule>) -> ParserResult<TableColumn> {
Expand All @@ -232,19 +229,34 @@ fn parse_table_col(pair: Pair<Rule>) -> ParserResult<TableColumn> {
})
}

fn build_type_name_with_schema(schema: Option<&Ident>, type_name: Pair<Rule>) -> String {
let mut type_name = type_name.as_str().to_string();
if let Some(schema) = schema {
type_name = format!("{}.{}", schema.to_string, type_name);
}
type_name
}

fn parse_col_type(pair: Pair<Rule>) -> ParserResult<ColumnType> {
let mut out = ColumnType {
span_range: s2r(pair.as_span()),
raw: pair.as_str().to_string(),
..Default::default()
};

let mut schema = None;

for p1 in pair.into_inner() {
match p1.as_rule() {
Rule::ident => {
schema = Some(parse_ident(p1)?);
}
Rule::col_type_quoted | Rule::col_type_unquoted => {
for p2 in p1.into_inner() {
match p2.as_rule() {
Rule::var | Rule::spaced_var => out.type_name = ColumnTypeName::Raw(p2.as_str().to_string()),
Rule::var | Rule::spaced_var => {
out.type_name = ColumnTypeName::Raw(build_type_name_with_schema(schema.as_ref(), p2))
}
Rule::col_type_arg => out.args = parse_col_type_arg(p2)?,
Rule::col_type_array => {
let val = p2.into_inner().try_fold(None, |_, p3| {
Expand Down Expand Up @@ -547,17 +559,36 @@ fn parse_table_group_decl(pair: Pair<Rule>) -> ParserResult<TableGroupBlock> {

acc.items.push(init)
}
_ => throw_rules(&[Rule::decl_ident], p2)?,
Rule::note_decl => acc.note = Some(parse_note_decl(p2)?),
_ => throw_rules(&[Rule::decl_ident, Rule::note_decl], p2)?,
}
}
}
_ => throw_rules(&[Rule::ident, Rule::table_group_block], p1)?,
Rule::block_settings => {
acc.settings = Some(parse_table_group_settings(p1)?);
}
_ => throw_rules(&[Rule::ident, Rule::table_group_block, Rule::block_settings], p1)?,
}

Ok(acc)
})
}

fn parse_table_group_settings(pair: Pair<Rule>) -> ParserResult<TableGroupSettings> {
Ok(TableGroupSettings {
span_range: s2r(pair.as_span()),
attributes: pair
.into_inner()
.map(|p1| {
match p1.as_rule() {
Rule::attribute => parse_attribute(p1),
_ => throw_rules(&[Rule::attribute], p1),
}
})
.collect::<ParserResult<_>>()?,
})
}

fn parse_rel_settings(pair: Pair<Rule>) -> ParserResult<RefSettings> {
let init = RefSettings {
span_range: s2r(pair.as_span()),
Expand Down Expand Up @@ -922,7 +953,14 @@ pub fn parse_attribute(pair: Pair<Rule>) -> ParserResult<Attribute> {
value: parse_value(p1)?,
})
}
_ => throw_rules(&[Rule::value, Rule::spaced_var], p1)?,
Rule::double_quoted_string => {
init.value = Some(Literal {
span_range: s2r(p1.as_span()),
raw: p1.as_str().to_string(),
value: Value::String(p1.into_inner().as_str().to_string()),
})
}
_ => throw_rules(&[Rule::value, Rule::spaced_var, Rule::double_quoted_string], p1)?,
}
}

Expand Down
4 changes: 3 additions & 1 deletion tests/dbml/array_type.in.dbml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ Table sal_emp {
name text
pay_by_quarter "int[]" [not null]
schedule "text[][]" [null]
unquoted text[] [not null]
mixed "character varying"[]
}

Table tictactoe {
squares "integer[3][3]"
}
}
2 changes: 1 addition & 1 deletion tests/dbml/index_tables.in.dbml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ Table users {
(`getdate()`, `upper(gender)`)
(`reverse(country_code)`)
}
}
}
4 changes: 3 additions & 1 deletion tests/dbml/postgres_importer/multiple_schema.out.dbml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Table "users" {
"pjs2" job_status
"pg" schemaB.gender
"pg2" gender
"pg3" schemaB."gender"
"pg4" "schemaB"."gender"
}

Table "products" {
Expand Down Expand Up @@ -66,4 +68,4 @@ Table "schemaA"."locations" {
"id" int [pk]
"name" varchar
Note: 'This is a note in table "locations"'
}
}
6 changes: 5 additions & 1 deletion tests/dbml/sample_1.dbml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ Project project_name {
Note: 'Description of the project'
}

Note {
'A top level sticky note'
}

TableGroup order {
public.users // users
// posts
Expand Down Expand Up @@ -64,4 +68,4 @@ enum grade {
"Not Yet Set"
}

// Ref: posts.user_id > users.id [update: restrict] // many-to-one
// Ref: posts.user_id > users.id [update: restrict] // many-to-one
11 changes: 9 additions & 2 deletions tests/dbml/table_group.in.dbml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ Table merchants {
admin_id int [ref: > U.id] // inline relationship (many-to-one)
}

TableGroup g1 {
TableGroup g1 [color: #abcdef] {
users
merchants
}

note: 'A table group description'
}


Note {
'Some note'
}
71 changes: 62 additions & 9 deletions tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,42 @@ fn read_dbml_dir<P: AsRef<Path>>(dir_path: P) -> Result<Vec<PathBuf>> {
Ok(out)
}

fn create_out_dir() -> Result<()> {
fn create_out_dir(sub_dir: Option<&PathBuf>) -> Result<()> {
if fs::metadata(OUT_DIR).is_err() {
fs::create_dir(OUT_DIR)?;
}
if let Some(sub_dir) = sub_dir {
let path = Path::new(OUT_DIR).join(sub_dir);
if fs::metadata(&path).is_err() {
fs::create_dir(path)?;
}
}

Ok(())
}

#[test]
fn parse_dbml_unchecked() -> Result<()> {
create_out_dir()?;
/// Run with UPDATE_DBML_OUTPUT=1 to update the expected output files
/// e.g. (on Linux or macOS)
/// UPDATE_DBML_OUTPUT=1 cargo test
fn update_expected() -> bool {
match std::env::var("UPDATE_DBML_OUTPUT") {
Ok(v) => v == "1",
_ => false,
}
}

fn compare_parsed_with_expected(sub_dir: Option<impl Into<PathBuf>>, update: bool) -> Result<()> {
let sub_dir = sub_dir.map(Into::into);

let testing_dbml_paths = read_dbml_dir("tests/dbml")?;
create_out_dir(sub_dir.as_ref())?;
let mut source_dir = Path::new("tests/dbml").to_path_buf();
let mut out_file_dir = Path::new(OUT_DIR).to_path_buf();
if let Some(sub_dir) = sub_dir {
source_dir.push(&sub_dir);
out_file_dir.push(&sub_dir);
}

let testing_dbml_paths = read_dbml_dir(source_dir)?;

for path in testing_dbml_paths {
let content = fs::read_to_string(&path)?;
Expand All @@ -46,14 +69,44 @@ fn parse_dbml_unchecked() -> Result<()> {
let mut out_file_path = path.clone();
out_file_path.set_extension("ron");
let out_file_name = out_file_path.file_name().unwrap().to_str().unwrap();
let out_file_path = format!("{}/{}", OUT_DIR, out_file_name);

fs::write(out_file_path, out_content)?;
let out_file_path = out_file_dir.join(out_file_name);

if update {
fs::write(out_file_path, out_content)?;
} else {
let expected = fs::read_to_string(&out_file_path).or_else(|e| {
match e.kind() {
std::io::ErrorKind::NotFound => Ok("no output file".to_string()),
e => Err(e),
}
})?;
assert_eq!(out_content, expected, "Unexpected output for {:?}", path);
}
}

Ok(())
}


#[test]
fn parse_dbml_root() -> Result<()> {
compare_parsed_with_expected(None::<PathBuf>, update_expected())
}

#[test]
fn parse_dbml_mysql_importer() -> Result<()> {
compare_parsed_with_expected(Some("mysql_importer"), update_expected())
}

#[test]
fn parse_dbml_mssql_importer() -> Result<()> {
compare_parsed_with_expected(Some("mssql_importer"), update_expected())
}

#[test]
fn parse_dbml_pgsql_importer() -> Result<()> {
compare_parsed_with_expected(Some("postgres_importer"), update_expected())
}

#[test]
fn parse_dbml_validator() -> Result<()> {
let testing_dbml_paths = read_dbml_dir("tests/dbml/validator")?;
Expand Down
Loading

0 comments on commit 7d3b0db

Please sign in to comment.