The text model format

A model is a directory of .bluep files. The format is line-oriented, has one canonical serialization and no comment syntax (documentation goes in description and comment strings), so the same model is always written the same way and a git diff shows only what changed.

Lexical structure

newline      = "\n" ;                                   (* "\r\n" is accepted on input, never written *)
space        = " " | "\t" ;
bare_ident   = ( letter | "_" ) , { letter | digit | "_" } ;
letter       = "A".."Z" | "a".."z" ;
digit        = "0".."9" ;
string       = '"' , { str_char | escape } , '"' ;
str_char     = ? any char except '"', '\', newline ? ;
escape       = '\"' | "\\" | "\n" | "\t" ;             (* exactly these four *)
raw_string   = '"""' , newline , { ? any char ? } , newline , '"""' ;  (* content excludes the two newlines *)
number       = [ "-" ] , digit , { digit } , [ "." , digit , { digit } ] ;
punct        = "{" | "}" | "(" | ")" | ":" | "," | "->" | "." | "[]" ;

A name is a bare_ident or a string. It is written bare when it matches [A-Za-z_][A-Za-z0-9_]* and is not one of the structural words below; otherwise it is quoted. Structural words are recognised only by position, so entity order { is valid because order stands in name position (and order is not a structural word).

Structural words

model naming lint domain enum term entity relationship area target schema table view sequence enum_type
column attribute key primary unique check foreign index collation exclusion extension policy

A name equal to one of these is always written quoted (entity "index" {). All other words of the grammar are positional and may be used bare as names.

Grammar

file          = { newline } , { declaration , { newline } } ;
declaration   = model_decl | naming_decl | lint_decl | domain_decl | enum_decl | term_decl
              | entity_decl | relationship_decl | area_decl | target_decl ;
name          = bare_ident | string ;
nl            = newline , { newline } ;
block_open    = "{" , nl ;
block_close   = "}" ;

model_decl    = "model" , name , block_open , { model_item , nl } , block_close ;
model_item    = "version" , string | "description" , string | "property" , name , string ;

naming_decl   = "naming" , block_open , { naming_rule , nl } , block_close ;
naming_rule   = object_kind , block_open , { naming_item , nl } , block_close ;
object_kind   = "entity" | "attribute" | "relationship" | "domain" | "enum" | "subject_area" | "term"
              | "schema" | "table" | "column" | "primary_key" | "unique_constraint" | "check_constraint"
              | "foreign_key" | "index" | "sequence" | "enum_type" | "view" ;
naming_item   = "case" , case_style | "pattern" , string | "prefix" , string | "suffix" , string
              | "max_length" , number | "forbidden" , string ;
case_style    = "snake" | "upper_snake" | "lower" | "upper" | "camel" | "pascal" ;

lint_decl     = "lint" , block_open , { name , severity , nl } , block_close ;
severity      = "off" | "warn" | "error" ;

domain_decl   = "domain" , name , block_open , { domain_item , nl } , block_close ;
domain_item   = "type" , logical_type | "not" , "null" | "default" , default_value | "check" , string
              | "description" , string | "physical" , dialect , phys_type ;

enum_decl     = "enum" , name , block_open , { enum_item , nl } , block_close ;
enum_item     = "description" , string | "value" , string ;

term_decl     = "term" , name , block_open , { term_item , nl } , block_close ;
term_item     = "definition" , string | "alias" , string ;

entity_decl   = "entity" , name , block_open , { entity_item , nl } , block_close ;
entity_item   = "description" , string | "tag" , string | "term" , name | attribute | key ;
attribute     = "attribute" , name , ":" , type_ref , { attr_opt } ;
type_ref      = logical_type | "domain" , name ;
attr_opt      = "not" , "null" | "default" , default_value | "term" , name | "description" , string ;
key           = "key" , ( "primary" | "unique" ) , name , "(" , name_list , ")" ;
name_list     = name , { "," , name } ;

logical_type  = "smallint" | "integer" | "bigint" | "decimal" , "(" , number , "," , number , ")"
              | "float" | "double" | "boolean" | "text" , [ "(" , number , ")" ] | "char" , "(" , number , ")"
              | "date" | "time" | "timestamp" | "timestamptz" | "uuid" | "binary" , [ "(" , number , ")" ]
              | "json" | "enum" , name ;

default_value = "null" | "true" | "false" | number | string | "expr" , string ;

relationship_decl = "relationship" , name , block_open , { rel_item , nl } , block_close ;
rel_item      = "description" , string | "parent" , name , [ "key" , name ] | "child" , name
              | "columns" , "(" , pair , { "," , pair } , ")"
              | "cardinality" , parent_card , "to" , child_card | "identifying"
              | "on" , "delete" , ref_action | "on" , "update" , ref_action ;
pair          = name , "->" , name ;                    (* child attribute -> parent attribute *)
parent_card   = "one" | "zero_or_one" ;
child_card    = "many" | "one_or_many" | "zero_or_one" | "one" ;
ref_action    = "no_action" | "restrict" | "cascade" | "set_null" | "set_default" ;

area_decl     = "area" , name , block_open , { area_item , nl } , block_close ;
area_item     = "description" , string | "entity" , name , [ "at" , number , number ] ;

target_decl   = "target" , name , ":" , dialect , block_open , { schema_decl , nl } , block_close ;
dialect       = "postgresql" | "sqlite" | "mysql" | "sqlserver" | "oracle"
              | "snowflake" | "bigquery" | "redshift" | "databricks" ;
schema_decl   = "schema" , name , block_open , { schema_item , nl } , block_close ;
schema_item   = "comment" , string | enum_type_decl | sequence_decl | table_decl | view_decl ;

enum_type_decl = "enum_type" , name , "(" , string , { "," , string } , ")" ;
sequence_decl = "sequence" , name , block_open , { seq_item , nl } , block_close ;
seq_item      = "start" , number | "increment" , number | "min_value" , number | "max_value" , number
              | "cycle" | "type" , phys_type ;

table_decl    = "table" , name , block_open , { table_item , nl } , block_close ;
table_item    = "entity" , name | "comment" , string | "option" , name , string | column
              | "primary" , "key" , [ name ] , "(" , name_list , ")"
              | "unique" , name , "(" , name_list , ")"
              | "check" , name , string
              | "foreign" , "key" , name , "(" , name_list , ")" , "references" , [ name , "." ] , name ,
                "(" , name_list , ")" , [ "on" , "delete" , ref_action ] , [ "on" , "update" , ref_action ] ,
                [ "deferrable" ] , [ "relationship" , name ]
              | "index" , name , [ "unique" ] , "(" , index_col , { "," , index_col } , ")" ,
                [ "using" , name ] , [ "include" , "(" , name_list , ")" ] , [ "where" , string ] ;
column        = "column" , name , phys_type , { col_opt } ;
col_opt       = "not" , "null" | "default" , default_value
              | "identity" , ( "always" | "by_default" ) , [ "(" , number , "," , number , ")" ]
              | "generated" , ( "stored" | "virtual" ) , string
              | "option" , string , string ;

Phase 26 extends identity declarations with start, increment, min, max, cache, and cycle in that order. Index declarations may carry kind fulltext|spatial, option pairs, and a comment. Targets contain extensions; schemas contain collations; tables contain exclusion constraints and row policies. Constraint, sequence, enum, and index comments use the existing quoted-string rules. Empty/default additions are omitted, so canonical files written before version 26 remain byte-identical. | "collate" , string | "attribute" , name | "comment" , string ; phys_type = bare_ident , [ "(" , type_arg , { "," , type_arg } , ")" ] , [ "[]" ] | string ; type_arg = number | string ; index_col = name , [ "desc" ] | "expr" , string , [ "desc" ] ;

view_decl     = "view" , name , block_open , { view_item , nl } , block_close ;
view_item     = "comment" , string | "sql" , raw_string ;

A phys_type written as a string is parsed with the generic physical type parser and is used whenever the type has modifiers, for example "timestamp(3) with time zone" or "int unsigned". A bare phys_type has no modifiers.

A domain check expression refers to the column it will be applied to through the identifier value; the physical check constraint substitutes the real column name.

Canonical serialization

  1. UTF-8, \n line endings, the file ends with exactly one \n. Two spaces of indentation per nesting level. No trailing whitespace. Exactly one space between tokens on a line; no space before , and ), and no space before ( except in key, primary key, unique, foreign key, index, columns and enum_type as shown in the example below.
  2. Top-level declarations are separated by exactly one blank line. There are no blank lines inside blocks.
  3. Declaration order within a file: model, naming, lint, then domain, enum, term, entity, relationship, area, target*; within a kind sorted by name (byte order). naming rules are sorted by object kind in the order of the object_kind production; lint rules by rule id.
  4. Items inside a block follow the order of the alternatives in the grammar production. Single-valued items appear once; property and option are sorted by key; tag, term, alias, value and forbidden keep their stored order (sets sorted, lists as declared); attributes and columns as declared; key primary before key unique, unique keys sorted by name. Table items: entity, comment, option, column, primary key, unique, check, foreign key, index, each group sorted by name. Schema items: comment, enum_type, sequence, table, view*.
  5. Attribute and column options in this order: not null, default, identity, collate, attribute, term*, description or comment. A nullable column is written by omitting not null.
  6. default is written as null, true, false, the number verbatim as stored, "string", or expr "sql".
  7. Strings use the four escapes only. View SQL always uses a raw_string; no other item does.
  8. Optional strings equal to the empty string are written (description ""); absent ones are omitted. no_action is omitted in relationships and foreign keys. identity start and increment are written only when either is set (identity always (1, 1), a missing value defaulting to 1).
  9. Names are written bare when possible, otherwise quoted.
  10. Integers have no leading zeros or +; at takes two integers.

Multiple files

A model directory contains model.bluep (required; only it may contain model, naming and lint) and any number of other *.bluep files found recursively, loaded in sorted relative-path order. A name declared in two files is an error naming both places, for example areas/x.bluep:1:8: entity 'customer' already declared in model.bluep:12:8. On write, every declaration goes back to the file it came from; new declarations go to model.bluep; a file that would become empty is deleted; files are written only when their content changes.

Example

model commerce {
  version "1.0"
  description "Online shop: customers, products, orders."
}

naming {
  column {
    case snake
    max_length 63
  }
  foreign_key {
    prefix "fk_"
  }
}

lint {
  missing-primary-key error
  unindexed-foreign-key warn
}

domain email {
  type text(320)
  not null
  check "value ~ '^[^@]+@[^@]+$'"
  description "RFC 5321 address, case preserved."
}

enum order_status {
  value "pending"
  value "paid"
  value "shipped"
  value "cancelled"
}

entity customer {
  description "A registered buyer."
  attribute id : bigint not null
  attribute email : domain email
  attribute full_name : text(200) not null
  attribute created_at : timestamptz not null default expr "now()"
  key primary pk_customer (id)
  key unique uq_customer_email (email)
}

entity order {
  attribute id : bigint not null
  attribute customer_id : bigint not null
  attribute status : enum order_status not null default "pending"
  attribute placed_at : timestamptz not null default expr "now()"
  key primary pk_order (id)
}

relationship order_customer {
  parent customer
  child order
  columns (customer_id -> id)
  cardinality one to many
  on delete restrict
}

area "Sales" {
  description "Ordering flow."
  entity customer at 40 40
  entity order at 360 40
}

target pg : postgresql {
  schema public {
    enum_type order_status ("pending", "paid", "shipped", "cancelled")
    table customer {
      entity customer
      comment "A registered buyer."
      column id bigint not null identity always attribute id
      column email varchar(320) not null attribute email
      column full_name varchar(200) not null attribute full_name
      column created_at "timestamp with time zone" not null default expr "now()" attribute created_at
      primary key pk_customer (id)
      unique uq_customer_email (email)
      check ck_customer_email "email ~ '^[^@]+@[^@]+$'"
    }
    table order {
      entity order
      column id bigint not null identity always attribute id
      column customer_id bigint not null attribute customer_id
      column status order_status not null default "pending" attribute status
      column placed_at "timestamp with time zone" not null default expr "now()" attribute placed_at
      primary key pk_order (id)
      foreign key fk_order_customer (customer_id) references customer (id) on delete restrict relationship order_customer
      index ix_order_customer (customer_id)
    }
  }
}

Errors

Syntax errors stop at the first problem and are reported as <file>:<line>:<col>: <message>, for example model.bluep:2:16: expected ':' after attribute name or model.bluep:5:18: unknown logical type 'bigintt'. Semantic problems (dangling references, duplicate names, mismatched relationship pairs) are reported by bluep check one per line as <kind> '<name>': <text>.

Properties and classifications

Two further top-level declarations, property and classification, and two items, prop and classify, extend the format. property and classification are structural words (a name equal to one of them is quoted). Canonical order places property* and classification* after lint and before domain*.

declaration    = model_decl | naming_decl | lint_decl | property_decl | classification_decl
               | domain_decl | enum_decl | term_decl | entity_decl | relationship_decl | area_decl | target_decl ;

property_decl  = "property" , name , block_open , { property_item , nl } , block_close ;
property_item  = "type" , prop_type | "description" , string | "default" , prop_value
               | "applies" , object_kind , { "," , object_kind } | "required" ;
prop_type      = "text" | "integer" | "boolean" | "list"
               | "choice" , "(" , string , { "," , string } , ")" ;
prop_value     = string | number | "true" | "false" | "(" , [ string , { "," , string } ] , ")" ;

classification_decl = "classification" , name , block_open , { class_item , nl } , block_close ;
class_item     = "description" , string | "level" , string | "required" | "inherit" ;

prop_item      = "prop" , name , prop_value ;
classify_item  = "classify" , name , string ;

object_kind    = (* the 18 kinds of section 3.3 *) | "property" | "classification" ;   (* naming blocks only *)

domain_item    += prop_item ;
enum_item      += prop_item ;
term_item      += prop_item ;
entity_item    += classify_item | prop_item ;
attr_opt       += "classify" , name , string | "prop" , name , prop_value ;
rel_item       += prop_item ;
area_item      += prop_item ;
target_decl    = "target" , name , ":" , dialect , block_open , { ( prop_item | schema_decl ) , nl } , block_close ;
schema_item    += prop_item ;
table_item     += classify_item | prop_item ;
col_opt        += "classify" , name , string | "prop" , name , prop_value ;
view_item      += prop_item ;
seq_item       += prop_item ;
Block / line Position
domain type, not null, default, check, description, prop*, physical*
enum description, prop*, value*
term definition, prop*, alias*
entity description, classify*, prop*, tag*, term*, attributes, keys
attribute line not null, default, classify*, prop*, term*, description
relationship description, prop*, parent, child, columns, cardinality, identifying, on delete, on update
area description, prop*, entity*
target prop*, schema*
schema comment, prop*, enum_type*, sequence*, table*, view*
table entity, comment, classify*, prop*, option*, column*, primary key, unique*, check*, foreign key*, index*
column line not null, default, identity, collate, attribute, classify*, prop*, comment
view comment, prop*, sql
sequence prop*, start, increment, min_value, max_value, cycle, type

Procedural and security objects

Targets carry roles, schemas carry routines, materialized views and grants, and tables carry triggers and partitioning. View SQL, materialized view SQL, routine bodies and trigger bodies use raw_string.

target_decl    = "target" , name , ":" , dialect , block_open , { ( role_decl | schema_decl ) , nl } , block_close ;
role_decl      = "role" , name , block_open , { role_item , nl } , block_close ;
role_item      = "login" | "member_of" , name | "option" , name , string ;
schema_item    += routine_decl | matview_decl | grant_decl ;
routine_decl   = ( "function" | "procedure" ) , name , "(" , [ arg , { "," , arg } ] , ")" ,
                 block_open , { routine_item , nl } , block_close ;
arg            = [ "in" | "out" | "inout" ] , [ name ] , phys_type , [ "default" , string ] ;
routine_item   = "returns" , string | "language" , name | "option" , name , string | "comment" , string
               | prop_item | "body" , raw_string ;
matview_decl   = "materialized_view" , name , block_open , { matview_item , nl } , block_close ;
matview_item   = "comment" , string | prop_item | "no" , "data" | "sql" , raw_string
               | "index" , name , [ "unique" ] , "(" , index_col , { "," , index_col } , ")" , [ "using" , name ] , [ "where" , string ] ;
grant_decl     = "grant" , privilege , { "," , privilege } , "on" , grant_object , "to" , name , [ "with" , "grant" , "option" ] ;
privilege      = "select" | "insert" | "update" | "delete" | "truncate" | "references" | "trigger" | "usage"
               | "execute" | "create" | "all" ;
grant_object   = "schema" | "all" , "tables" | "all" , "sequences" | "all" , "routines"
               | "table" , name | "view" , name | "sequence" , name | "routine" , name ;
table_item     += trigger_decl | partition_decl ;
trigger_decl   = "trigger" , name , block_open , { trigger_item , nl } , block_close ;
trigger_item   = "timing" , ( "before" | "after" | "instead_of" ) | "events" , event , { "," , event }
               | "update_columns" , "(" , name_list , ")" | "for" , "each" , ( "row" | "statement" )
               | "when" , string | "function" , name | "comment" , string | "body" , raw_string ;
event          = "insert" | "update" | "delete" | "truncate" ;
partition_decl = "partition" , "by" , ( "range" | "list" | "hash" ) , "(" , index_col , { "," , index_col } , ")" ,
                 block_open , { "partition" , name , string , nl } , block_close ;

function, procedure, trigger, materialized_view, grant and role are structural words: a name equal to one of them is written quoted. See docs/procedural.md for the canonical item order and the SQL produced for each dialect.

Notation and stereotypes

A model may choose the diagram notation, a subject area may override it for its own diagram, and an entity may carry a dimensional-modeling role:

model_item     = "version" , string | "description" , string | "notation" , notation | "property" , name , string ;
area_item      = "description" , string | "notation" , notation | prop_item | "entity" , name , [ "at" , number , number ] ;
notation       = "crows_foot" | "idef1x" ;
entity_item    = "description" , string | "stereotype" , stereotype | classify_item | prop_item
               | "tag" , string | "term" , name | attribute | key ;
stereotype     = "fact" | "dimension" | "bridge" ;

The words notation, stereotype, crows_foot, idef1x, fact, dimension and bridge are recognised by position only, so they remain usable as bare names. A block may hold at most one notation (or stereotype). An unknown value is a format error at its position listing the accepted words.

Canonical positions:

  • model block: version, description, notation, property*.
  • area block: description, notation, prop, entity.
  • entity block: description, stereotype, classify, prop, tag, term, attributes, keys.

Both items are written as notation idef1x and stereotype fact, and omitted when unset. In JSON they are the keys notation (on metadata and on each subject area) and stereotype (on each entity). See docs/merge.md for how the notations are drawn.

Many-to-many associations

Place many_to_many blocks after ordinary relationships and before hierarchies. Each block has a left and right end with ordered column -> referenced_attribute pairs. An end can name a key and can be optional; extra attribute lines describe payload columns on the associative table. Optional inferred, description, and prop lines follow the attributes.

many_to_many enrollment {
  left student (student_id -> id)
  right course (course_id -> id)
  attribute enrolled_at : timestamp not null
}

Generalization hierarchies

Place hierarchy blocks after relationships and before areas. Each block declares a supertype, optional discriminator, explicit category, and subtype lines in declaration order. Target transformations are written as hierarchy payment_method transform rollup; identity is implicit.

  • Target generation profiles and model/schema/table SQL hooks are documented in Custom generation.