Module: DBML::Parser

Extended by:
Rsec::Helpers
Defined in:
lib/dbml.rb

Constant Summary collapse

RESERVED_PUNCTUATION =
%q{`"':\[\]\{\}\(\)\>\<,.}
NAKED_IDENTIFIER =
/[^#{RESERVED_PUNCTUATION}\s]+/.r
QUOTED_IDENTIFIER =
'"'.r >> /[^"]+/.r << '"'.r
IDENTIFIER =
QUOTED_IDENTIFIER | NAKED_IDENTIFIER
BOOLEAN =

ATOM parses true: ‘true’ => true ATOM parses false: ‘false’ => false ATOM parses null: ‘null’ => nil ATOM parses numbers: ‘123.45678’ => 123.45678 ATOM parses strings: “‘string’” => “string” ATOM parses multilines: “”‘longnstring”’” => “longnstring” ATOM parses expressions: ‘`now()`’ => DBML::Expression.new(‘now()’)

'true'.r.map {|_| true } | 'false'.r.map {|_| false }
NULL =
'null'.r.map {|_| nil }
NUMBER =
prim(:double)
EXPRESSION =
seq('`'.r, /[^`]*/.r, '`'.r)[1].map {|str| Expression.new str}
KEYWORD =

KEYWORD parses phrases: ‘no action’ => :“no action”

/[^#{RESERVED_PUNCTUATION}\s][^#{RESERVED_PUNCTUATION}]*/.r.map {|str| str.to_sym}
SINGLE_LING_STRING =
seq("'".r, /[^']*/.r, "'".r)[1]
MULTI_LINE_STRING =

MULTI_LINE_STRING ignores indentation on the first line: “”‘ longn string”’” => “longn string” MULTI_LINE_STRING allows apostrophes: “”‘it’s a string with ” bunny ears”‘” => “it’s a string with ” bunny ears” MULTI_LINE_STRING allows blanks: “”””” => “” MULTI_LINE_STRING allows blank lines to have no indent: “”‘ my stringnn it’s really great”‘” => “my stringnit’s really great” MULTI_LINE_STRING allows blank first lines: “”‘n start ofn my writing”’” => “start ofnmy writing”

seq(/'''\n?/.r, /([^']|'[^']|''[^'])*/m.r, /\n?'''/.r)[1].map do |string|
  indent = string.match(/^\s*/m)[0].size
  string.lines.map do |line|
    raise "Indentation does not match in #{line.inspect}" unless line =~ /\s{#{indent}}/ or line =~ /^\n$/
    line[indent..]
  end.join
end
STRING =

STRING parses blank strings: “”” => “” STRING parses double quotes: ‘“”’ => “”

MULTI_LINE_STRING | SINGLE_LING_STRING
ATOM =
BOOLEAN | NULL | NUMBER | EXPRESSION | STRING
REF_SETTING =

Each setting item can take in 2 forms: Key: Value or keyword, similar to that of Python function parameters. Settings are all defined within square brackets: [setting1: value1, setting2: value2, setting3, setting4]

SETTINGS parses key value settings: ‘[default: 123]’ => 123 SETTINGS parses keyword settings: ‘[not null]’ => null’ => nil SETTINGS parses many settings: “[some setting: ‘value’, primary key]” => setting’ => ‘value’, :‘primary key’ => nil SETTINGS parses keyword values: “[delete: cascade]” => :cascade SETTINGS parses relationship form: ‘[ref: > users.id]’ => [DBML::Relationship.new(nil, nil, [], ‘>’, ‘users’, [‘id’], {)]} SETTINGS parses multiple relationships: ‘[ref: > a.b, ref: < c.d]’ => [DBML::Relationship.new(nil, nil, [], ‘>’, ‘a’, [‘b’], {), DBML::Relationship.new(nil, nil, [], ‘<’, ‘c’, [‘d’], {})]}

'ref:'.r >> seq_(lazy { RELATIONSHIP_TYPE }, lazy {RELATIONSHIP_PART}).map do |(type, part)|
  Relationship.new(nil, nil, [], type, *part, {})
end
SETTING =
seq_(KEYWORD, (':'.r >> (ATOM | KEYWORD)).maybe(&method(:unwrap))) {|(key, value)| {key => value} }
SETTINGS =
('['.r >> comma_separated(REF_SETTING | SETTING) << ']'.r).map do |values|
  refs, settings = values.partition {|val| val.is_a? Relationship }
  [*settings, *(if refs.any? then [{ref: refs}] else [] end)].reduce({}, &:update)
end
NOTE =

NOTE parses short notes: “Note: ‘this is cool’” => ‘this is cool’ NOTE parses block notes: “Note a single line of note’n” => ‘still a single line of note’ NOTE can use multilines: “Note: ”‘this isnnot reassuring”’” => “this isnnot reassuring”

'Note'.r >> (long_or_short STRING)
INDEX_SINGLE =

Index Definition

Indexes allow users to quickly locate and access the data. Users can define single or multi-column indexes.

Table bookings {
  id integer
  country varchar
  booking_date date
  created_at timestamp

  indexes {
      (id, country) [pk] // composite primary key
      created_at [note: 'Date']
      booking_date
      (country, booking_date) [unique]
      booking_date [type: hash]
      (`id*2`)
      (`id*3`,`getdate()`)
      (`id*3`,id)
  }
}

There are 3 types of index definitions:

# Index with single field (with index name): CREATE INDEX on users (created_at) # Index with multiple fields (composite index): CREATE INDEX on users (created_at, country) # Index with an expression: CREATE INDEX ON films ( first_name + last_name ) # (bonus) Composite index with expression: CREATE INDEX ON users ( country, (lower(name)) )

INDEX parses single fields: ‘id’ => DBML::Index.new(, {}) INDEX parses composite fields: ‘(id, country)’ => DBML::Index.new([‘id’, ‘country’], {}) INDEX parses expressions: ‘(`id*2`)’ => :Index.new(, {}) INDEX parses expressions: ‘(`id*2`,`id*3`)’ => DBML::Index.new([DBML::Expression.new(‘id*2’), DBML::Expression.new(‘id*3’)], {}) INDEX parses naked ids and settings: “test_col [type: hash]” => DBML::Index.new(, :hash) INDEX parses settings: ‘(country, booking_date) [unique]’ => DBML::Index.new([‘country’, ‘booking_date’], nil) INDEXES parses empty block: ‘indexes { }’ => [] INDEXES parses single index: “indexes ncolumn_namen” => [DBML::Index.new(, {})] INDEXES parses multiple indexes: “indexes [pk]ntest_index [unique]n” => [DBML::Index.new(, nil), DBML::Index.new(, nil)]

IDENTIFIER
INDEX_COMPOSITE =
seq_('('.r, comma_separated(EXPRESSION | INDEX_SINGLE), ')'.r).inner.map {|v| unwrap(v) }
INDEX =
seq_(INDEX_SINGLE.map {|field| [field] } | INDEX_COMPOSITE, SETTINGS.maybe).map do |(fields, settings)|
  Index.new fields, unwrap(settings) || {}
end
INDEXES =
block 'indexes', ''.r, INDEX do |(_, indexes)| indexes end
ENUM_CHOICE =

Enum Definition


Enum allows users to define different values of a particular column.

enum job_status {
    created [note: 'Waiting to be processed']
    running
    done
    failure
}

ENUM parses empty blocks: “enum empty n” => DBML::Enum.new(‘empty’, []) ENUM parses settings: “enum setting [note: ‘something’]n” => DBML::Enum.new(‘setting’, [DBML::EnumChoice.new(‘one’, ‘something’)]) ENUM parses filled blocks: “enum filled nonentwo” => DBML::Enum.new(‘filled’, [DBML::EnumChoice.new(‘one’, {}), DBML::EnumChoice.new(‘two’, {})])

seq_(IDENTIFIER, SETTINGS.maybe).map {|(name, settings)| EnumChoice.new name, unwrap(settings) || {} }
ENUM =
block 'enum', IDENTIFIER, ENUM_CHOICE do |(name, choices)|
  Enum.new name, choices
end
COLUMN_NAME =

Column Definition

  • name of the column is listed as column_name

  • type of the data in the column listed as column_type

  • supports all data types, as long as it is a single word (remove all spaces in the data type). Example, JSON, JSONB, decimal(1,2), etc.

  • column_name can be stated in just plain text, or wrapped in a double quote as “column name”

Column Settings


Each column can take have optional settings, defined in square brackets like:

Table buildings {
    //...
    address varchar(255) [unique, not null, note: 'to include unit number']
    id integer [ pk, unique, default: 123, note: 'Number' ]
}

COLUMN parses naked identifiers as names: ‘column_name type’ => DBML::Column.new(‘column_name’, ‘type’, {}) COLUMN parses quoted identifiers as names: ‘“column name” type’ => DBML::Column.new(‘column name’, ‘type’, {}) COLUMN parses types: ‘name string’ => DBML::Column.new(‘name’, ‘string’, {}) COLUMN parses settings: ‘name string [pk]’ => DBML::Column.new(‘name’, ‘string’, nil)

IDENTIFIER
COLUMN_TYPE =
/[^\s\{\}]+/.r
COLUMN =
seq_(COLUMN_NAME, COLUMN_TYPE, SETTINGS.maybe) do |(name, type, settings)|
  Column.new name, type, unwrap(settings) || {}
end
TABLE_NAME =

Table Definition

Table table_name {
  column_name column_type [column_settings]
}
  • title of database table is listed as table_name

  • list is wrapped in curly brackets {}, for indexes, constraints and table definitions.

  • string value is be wrapped in a single quote as ‘string’

TABLE_NAME parses identifiers: ‘table_name’ => [‘table_name’, nil] TABLE_NAME parses aliases: ‘table_name as thingy’ => [‘table_name’, ‘thingy’] TABLE parses empty tables: ‘Table empty {}’ => DBML::Table.new(‘empty’, nil, [], [], []) TABLE parses notes: “Table with_notes ‘this is a note’n” => DBML::Table.new(‘with_notes’, nil, [‘this is a note’], [], [])

seq_(IDENTIFIER, ('as'.r >> IDENTIFIER).maybe {|v| unwrap(v) })
TABLE =
block 'Table', TABLE_NAME, (INDEXES | NOTE | COLUMN) do |((name, aliaz), objects)|
  Table.new name, aliaz,
    objects.select {|o| o.is_a? String },
    objects.select {|o| o.is_a? Column },
    objects.select {|o| o.is_a? Index }
end
TABLE_GROUP =

TableGroup

TableGroup allows users to group the related or associated tables together.

TableGroup tablegroup_name { // tablegroup is case-insensitive.
    table1
    table2
    table3
}

TABLE_GROUP parses names: ‘TableGroup group1 { }’ => DBML::TableGroup.new(‘group1’, []) TABLE_GROUP parses tables: “TableGroup group2 ntable1ntable2n” => DBML::TableGroup.new(‘group2’, [‘table1’, ‘table2’])

block 'TableGroup', IDENTIFIER, IDENTIFIER do |(name, tables)|
  TableGroup.new name, tables
end
COMPOSITE_COLUMNS =

Relationships & Foreign Key Definitions

Relationships are used to define foreign key constraints between tables.

Table posts {
    id integer [primary key]
    user_id integer [ref: > users.id] // many-to-one
}

// or this
Table users {
    id integer [ref: < posts.user_id, ref: < reviews.user_id] // one to many
}

// The space after '<' is optional

There are 3 types of relationships: one-to-one, one-to-many, and many-to-one

1. <: one-to-many. E.g: users.id < posts.user_id
2. >: many-to-one. E.g: posts.user_id > users.id
3. -: one-to-one. E.g: users.id - user_infos.user_id

Composite foreign keys:

Ref: merchant_periods.(merchant_id, country_code) > merchants.(id, country_code)

In DBML, there are 3 syntaxes to define relationships:

//Long form
Ref name_optional {
  table1.column1 < table2.column2
}

//Short form:
Ref name_optional: table1.column1 < table2.column2

// Inline form
Table posts {
    id integer
    user_id integer [ref: > users.id]
}

Relationship settings

Ref: products.merchant_id > merchants.id [delete: cascade, update: no action]
  • delete / update: cascade | restrict | set null | set default | no action Define referential actions. Similar to ON DELETE/UPDATE CASCADE/… in SQL.

Relationship settings are not supported for inline form ref.

COMPOSITE_COLUMNS parses single column: ‘(column)’ => [‘column’] COMPOSITE_COLUMNS parses multiple columns: ‘(col1, col2)’ => [‘col1’, ‘col2’] RELATIONSHIP_PART parses simple form: ‘table.column’ => [‘table’, [‘column’]] RELATIONSHIP_PART parses composite form: ‘table.(a, b)’ => [‘table’, [‘a’, ‘b’]] RELATIONSHIP parses long form: “Ref name < right.rcoln” => DBML::Relationship.new(‘name’, ‘left’, [‘lcol’], ‘<’, ‘right’, [‘rcol’], {}) RELATIONSHIP parses short form: “Ref name: left.lcol > right.rcol” => DBML::Relationship.new(‘name’, ‘left’, [‘lcol’], ‘>’, ‘right’, [‘rcol’], {}) RELATIONSHIP parses composite form: ‘Ref: left.(a, b) - right.(c, d)’ => DBML::Relationship.new(nil, ‘left’, [‘a’, ‘b’], ‘-’, ‘right’, [‘c’, ‘d’], {}) RELATIONSHIP parses lowercase r: “ref name: left.lcol > right.rcol” => DBML::Relationship.new(‘name’, ‘left’, [‘lcol’], ‘>’, ‘right’, [‘rcol’], {}) RELATIONSHIP parses settings: “Ref: L.a > R.b [delete: cascade, update: no action]” => DBML::Relationship.new(nil, ‘L’, [‘a’], ‘>’, ‘R’, [‘b’], :cascade, update: :‘no action’)

'('.r >> comma_separated(COLUMN_NAME) << ')'
RELATIONSHIP_TYPE =
'>'.r | '<'.r | '-'.r
RELATIONSHIP_PART =
seq(seq(IDENTIFIER, '.'.r)[0], (COLUMN_NAME.map {|c| [c]}) | COMPOSITE_COLUMNS)
RELATIONSHIP_BODY =
seq_(RELATIONSHIP_PART, RELATIONSHIP_TYPE, RELATIONSHIP_PART, SETTINGS.maybe)
RELATIONSHIP =
seq_(/[Rr]ef/.r >> NAKED_IDENTIFIER.maybe, long_or_short(RELATIONSHIP_BODY)).map do |(name, (left, type, right, settings))|
  Relationship.new unwrap(name), *left, type, *right, unwrap(settings) || {}
end
PROJECT_DEFINITION =

Project Definition

You can give overall description of the project.

Project project_name {
  database_type: 'PostgreSQL'
  Note: 'Description of the project'
}

PROJECT_DEFINITION parses names: ‘Project my_proj { }’ => DBML::ProjectDef.new(‘my_proj’, [], {}) PROJECT_DEFINITION parses notes: “Project my_porg { Note: ‘porgs are cool!’ }” => DBML::ProjectDef.new(‘my_porg’, [‘porgs are cool!’], {}) PROJECT_DEFINITION parses settings: “Project my_cool ‘PostgreSQL’n” => DBML::ProjectDef.new(‘my_cool’, [], ‘PostgreSQL’)

block 'Project', IDENTIFIER, (NOTE | SETTING).star do |(name, objects)|
  ProjectDef.new name,
    objects.select {|o| o.is_a? String },
    objects.select {|o| o.is_a? Hash }.reduce({}, &:update)
end
PROJECT =

PROJECT can be empty: “” => DBML::Project.new(nil, [], {}, [], [], [], []) PROJECT includes definition info: “Project p { Note: ‘hello’ }” => DBML::Project.new(‘p’, [‘hello’], {}, [], [], [], []) PROJECT includes tables: “Table t { }” => DBML::Project.new(nil, [], {}, [DBML::Table.new(‘t’, nil, [], [], [])], [], [], []) PROJECT includes enums: “enum E { }” => DBML::Project.new(nil, [], {}, [], [], [DBML::Enum.new(‘E’, [])], []) PROJECT includes table groups: “TableGroup TG { }” => DBML::Project.new(nil, [], {}, [], [], [], [DBML::TableGroup.new(‘TG’, [])])

space_surrounded(PROJECT_DEFINITION | RELATIONSHIP | TABLE | TABLE_GROUP | ENUM).star do |objects|
  definition = objects.find {|o| o.is_a? ProjectDef }
  Project.new definition.nil? ? nil : definition.name,
    definition.nil? ? [] : definition.notes,
    definition.nil? ? {} : definition.settings,
    objects.select {|o| o.is_a? Table },
    objects.select {|o| o.is_a? Relationship },
    objects.select {|o| o.is_a? Enum },
    objects.select {|o| o.is_a? TableGroup }
end

Class Method Summary collapse

Class Method Details

.block(type, name_parser, content_parser, &block) ⇒ Object



34
35
36
# File 'lib/dbml.rb', line 34

def self.block type, name_parser, content_parser, &block
  seq_(type.r >> name_parser, '{'.r >> space_surrounded(content_parser).star.map {|a| a.flatten(1) } << '}'.r, &block)
end

.comma_separated(p) ⇒ Object



26
27
28
# File 'lib/dbml.rb', line 26

def self.comma_separated p
  p.join(/, */.r.map {|_| nil}).star.map {|v| (v.first || []).reject(&:nil?) }
end

.long_or_short(p) ⇒ Object



18
19
20
# File 'lib/dbml.rb', line 18

def self.long_or_short p
  (':'.r >> p) | ('{'.r >> p << '}'.r)
end

.parse(str) ⇒ Object



334
335
336
# File 'lib/dbml.rb', line 334

def self.parse str
  PROJECT.eof.parse! str.gsub(/\/{2}.*$/, '')
end

.space_surrounded(p) ⇒ Object



30
31
32
# File 'lib/dbml.rb', line 30

def self.space_surrounded p
  /\s*/.r >> p << /\s*/.r
end

.unwrap(p, *_) ⇒ Object



22
23
24
# File 'lib/dbml.rb', line 22

def self.unwrap p, *_
  if p.empty? then nil else p.first end
end