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
- UTF-8,
\nline 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 inkey,primary key,unique,foreign key,index,columnsandenum_typeas shown in the example below. - Top-level declarations are separated by exactly one blank line. There are no blank lines inside blocks.
- Declaration order within a file:
model,naming,lint, thendomain,enum,term,entity,relationship,area,target*; within a kind sorted by name (byte order).namingrules are sorted by object kind in the order of theobject_kindproduction;lintrules by rule id. - Items inside a block follow the order of the alternatives in the grammar production. Single-valued items
appear once;
propertyandoptionare sorted by key;tag,term,alias,valueandforbiddenkeep their stored order (sets sorted, lists as declared); attributes and columns as declared;key primarybeforekey 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*. - Attribute and column options in this order:
not null,default,identity,collate,attribute,term*,descriptionorcomment. A nullable column is written by omittingnot null. defaultis written asnull,true,false, the number verbatim as stored,"string", orexpr "sql".- Strings use the four escapes only. View SQL always uses a
raw_string; no other item does. - Optional strings equal to the empty string are written (
description ""); absent ones are omitted.no_actionis omitted in relationships and foreign keys.identitystart and increment are written only when either is set (identity always (1, 1), a missing value defaulting to 1). - Names are written bare when possible, otherwise quoted.
- Integers have no leading zeros or
+;attakes 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:
modelblock:version,description,notation,property*.areablock:description,notation,prop,entity.entityblock: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.
Related
- Target
generationprofiles and model/schema/table SQL hooks are documented in Custom generation.