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 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 optionalThere 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_idComposite 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
- .block(type, name_parser, content_parser, &block) ⇒ Object
- .comma_separated(p) ⇒ Object
- .long_or_short(p) ⇒ Object
- .parse(str) ⇒ Object
- .space_surrounded(p) ⇒ Object
- .unwrap(p, *_) ⇒ Object
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 |