dbml 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dbml.rb +132 -44
  3. data/lib/dbml/version.rb +1 -1
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8979759a1d1b3dc026d99323199313a0e35fca4fc0cf865a7ed256b1bf61054c
4
- data.tar.gz: 93f4d97a7c07c9d94e9412cc781c0312007b7dd42c225ffc9fd64aec3fc9eb1f
3
+ metadata.gz: 48c1cc0879db898fd838bccb388775692c2fd9510cc889dfb06aea5bc6532856
4
+ data.tar.gz: a6aa38aa4625fad6f0dafc7a49e1866f6376d222cf92dede1ea0994583312388
5
5
  SHA512:
6
- metadata.gz: 03d021e161615e622a88bac15a21c8fd633f60bed64b081d0479ee47df29478e5d9eb0465365269bf214c8f160bfb452de81e2e78114d033a60bf6d23131c087
7
- data.tar.gz: 32bb14bc1df49cebf682b7b65aa5670e45130f182f6397d3df45ec26744fe4e394aa7050ffe0071e86b0e389dd1f6370b39ee6ab9c48238d37cea4985ecd890b
6
+ metadata.gz: 4ec98ffd66c2c197329a74a44bbecf8766124a12fe1e809b121d6a9428e7378e6a65a4c7e93c62f8a94b9662f69da5a1985010e6452d1cf945fb49befcde6ac5
7
+ data.tar.gz: 2c1c230ed637641c8fc798cf04dbc21eb282bfbb428ae6fdeeb752602dc22fcab549712e208f2b52d0e7f119068fb49ab7d2b26d8a1b66e50cb18ed491fa2517
@@ -1,18 +1,20 @@
1
1
  require 'rsec'
2
- include Rsec::Helpers
3
2
 
4
3
  module DBML
5
- Column = Struct.new :name, :type, :settings
6
- Table = Struct.new :name, :alias, :notes, :columns, :indexes
7
- Index = Struct.new :fields, :settings
8
- Expression = Struct.new :text
9
- Enum = Struct.new :name, :choices
10
- EnumChoice = Struct.new :name, :settings
11
- TableGroup = Struct.new :name, :tables
12
- Project = Struct.new :name, :notes, :settings, :tables, :enums, :table_groups
13
- ProjectDef = Struct.new :name, :notes, :settings
4
+ Column = Struct.new :name, :type, :settings
5
+ Table = Struct.new :name, :alias, :notes, :columns, :indexes
6
+ Index = Struct.new :fields, :settings
7
+ Expression = Struct.new :text
8
+ Relationship = Struct.new :name, :left_table, :left_fields, :type, :right_table, :right_fields, :settings
9
+ Enum = Struct.new :name, :choices
10
+ EnumChoice = Struct.new :name, :settings
11
+ TableGroup = Struct.new :name, :tables
12
+ Project = Struct.new :name, :notes, :settings, :tables, :relationships, :enums, :table_groups
13
+ ProjectDef = Struct.new :name, :notes, :settings
14
14
 
15
15
  module Parser
16
+ extend Rsec::Helpers
17
+
16
18
  def self.long_or_short p
17
19
  (':'.r >> p) | ('{'.r >> p << '}'.r)
18
20
  end
@@ -22,7 +24,7 @@ module DBML
22
24
  end
23
25
 
24
26
  def self.comma_separated p
25
- p.join(/, */.r.map {|_| nil}).star.map {|v| v.first.reject(&:nil?) }
27
+ p.join(/, */.r.map {|_| nil}).star.map {|v| (v.first || []).reject(&:nil?) }
26
28
  end
27
29
 
28
30
  def self.space_surrounded p
@@ -33,6 +35,11 @@ module DBML
33
35
  seq_(type.r >> name_parser, '{'.r >> space_surrounded(content_parser).star.map {|a| a.flatten(1) } << '}'.r, &block)
34
36
  end
35
37
 
38
+ RESERVED_PUNCTUATION = %q{`"':\[\]\{\}\(\)\>\<,.}
39
+ NAKED_IDENTIFIER = /[^#{RESERVED_PUNCTUATION}\s]+/.r
40
+ QUOTED_IDENTIFIER = '"'.r >> /[^"]+/.r << '"'.r
41
+ IDENTIFIER = QUOTED_IDENTIFIER | NAKED_IDENTIFIER
42
+
36
43
  # ATOM parses true: 'true' => true
37
44
  # ATOM parses false: 'false' => false
38
45
  # ATOM parses null: 'null' => nil
@@ -44,10 +51,12 @@ module DBML
44
51
  NULL = 'null'.r.map {|_| nil }
45
52
  NUMBER = prim(:double)
46
53
  EXPRESSION = seq('`'.r, /[^`]+/.r, '`'.r)[1].map {|str| Expression.new str}
54
+ # KEYWORD parses phrases: 'no action' => :"no action"
55
+ KEYWORD = /[^#{RESERVED_PUNCTUATION}\s][^#{RESERVED_PUNCTUATION}]*/.r.map {|str| str.to_sym}
47
56
  SINGLE_LING_STRING = seq("'".r, /[^']+/.r, "'".r)[1]
57
+ # MULTI_LINE_STRING ignores indentation on the first line: "''' long\n string'''" => "long\n string"
58
+ # MULTI_LINE_STRING allows apostrophes: "'''it's a string with '' bunny ears'''" => "it's a string with '' bunny ears"
48
59
  MULTI_LINE_STRING = seq("'''".r, /([^']|'[^']|''[^'])+/m.r, "'''".r)[1].map do |string|
49
- # MULTI_LINE_STRING ignores indentation on the first line: "''' long\n string'''" => "long\n string"
50
- # MULTI_LINE_STRING allows apostrophes: "'''it's a string with '' bunny ears'''" => "it's a string with '' bunny ears"
51
60
  indent = string.match(/^\s*/m)[0].size
52
61
  string.lines.map do |line|
53
62
  raise "Indentation does not match" unless line =~ /\s{#{indent}}/
@@ -60,11 +69,20 @@ module DBML
60
69
  # Each setting item can take in 2 forms: Key: Value or keyword, similar to that of Python function parameters.
61
70
  # Settings are all defined within square brackets: [setting1: value1, setting2: value2, setting3, setting4]
62
71
  #
63
- # SETTINGS parses key value settings: '[default: 123]' => {'default' => 123}
64
- # SETTINGS parses keyword settings: '[not null]' => {'not null' => nil}
65
- # SETTINGS parses many settings: "[some setting: 'value', primary key]" => {'some setting' => 'value', 'primary key' => nil}
66
- SETTING = seq_(/[^,:\[\]\{\}\s][^,:\[\]]+/.r, (':'.r >> ATOM).maybe(&method(:unwrap))) {|(key, value)| {key => value} }
67
- SETTINGS = ('['.r >> comma_separated(SETTING) << ']'.r).map {|values| values.reduce({}, &:update) }
72
+ # SETTINGS parses key value settings: '[default: 123]' => {default: 123}
73
+ # SETTINGS parses keyword settings: '[not null]' => {:'not null' => nil}
74
+ # SETTINGS parses many settings: "[some setting: 'value', primary key]" => {:'some setting' => 'value', :'primary key' => nil}
75
+ # SETTINGS parses keyword values: "[delete: cascade]" => {delete: :cascade}
76
+ # SETTINGS parses relationship form: '[ref: > users.id]' => {ref: [DBML::Relationship.new(nil, nil, [], '>', 'users', ['id'], {})]}
77
+ # SETTINGS parses multiple relationships: '[ref: > a.b, ref: < c.d]' => {ref: [DBML::Relationship.new(nil, nil, [], '>', 'a', ['b'], {}), DBML::Relationship.new(nil, nil, [], '<', 'c', ['d'], {})]}
78
+ REF_SETTING = 'ref:'.r >> seq_(lazy { RELATIONSHIP_TYPE }, lazy {RELATIONSHIP_PART}).map do |(type, part)|
79
+ Relationship.new(nil, nil, [], type, *part, {})
80
+ end
81
+ SETTING = seq_(KEYWORD, (':'.r >> (ATOM | KEYWORD)).maybe(&method(:unwrap))) {|(key, value)| {key => value} }
82
+ SETTINGS = ('['.r >> comma_separated(REF_SETTING | SETTING) << ']'.r).map do |values|
83
+ refs, settings = values.partition {|val| val.is_a? Relationship }
84
+ [*settings, *(if refs.any? then [{ref: refs}] else [] end)].reduce({}, &:update)
85
+ end
68
86
 
69
87
  # NOTE parses short notes: "Note: 'this is cool'" => 'this is cool'
70
88
  # NOTE parses block notes: "Note {\n'still a single line of note'\n}" => 'still a single line of note'
@@ -103,9 +121,14 @@ module DBML
103
121
  # INDEX parses single fields: 'id' => DBML::Index.new(['id'], {})
104
122
  # INDEX parses composite fields: '(id, country)' => DBML::Index.new(['id', 'country'], {})
105
123
  # INDEX parses expressions: '(`id*2`)' => DBML::Index.new([DBML::Expression.new('id*2')], {})
106
- # INDEX parses settings: '(country, booking_date) [unique]' => DBML::Index.new(['country', 'booking_date'], {'unique' => nil})
124
+ # INDEX parses expressions: '(`id*2`,`id*3`)' => DBML::Index.new([DBML::Expression.new('id*2'), DBML::Expression.new('id*3')], {})
125
+ # INDEX parses naked ids and settings: "test_col [type: hash]" => DBML::Index.new(["test_col"], {type: :hash})
126
+ # INDEX parses settings: '(country, booking_date) [unique]' => DBML::Index.new(['country', 'booking_date'], {unique: nil})
127
+ # INDEXES parses empty block: 'indexes { }' => []
128
+ # INDEXES parses single index: "indexes {\ncolumn_name\n}" => [DBML::Index.new(['column_name'], {})]
129
+ # INDEXES parses multiple indexes: "indexes {\n(composite) [pk]\ntest_index [unique]\n}" => [DBML::Index.new(['composite'], {pk: nil}), DBML::Index.new(['test_index'], {unique: nil})]
107
130
 
108
- INDEX_SINGLE = /[^\(\)\,\{\}\s]+/.r
131
+ INDEX_SINGLE = IDENTIFIER
109
132
  INDEX_COMPOSITE = seq_('('.r, comma_separated(EXPRESSION | INDEX_SINGLE), ')'.r).inner.map {|v| unwrap(v) }
110
133
  INDEX = seq_(INDEX_SINGLE.map {|field| [field] } | INDEX_COMPOSITE, SETTINGS.maybe).map do |(fields, settings)|
111
134
  Index.new fields, unwrap(settings) || {}
@@ -125,11 +148,11 @@ module DBML
125
148
  # }
126
149
  #
127
150
  # ENUM parses empty blocks: "enum empty {\n}" => DBML::Enum.new('empty', [])
128
- # ENUM parses settings: "enum setting {\none [note: 'something']\n}" => DBML::Enum.new('setting', [DBML::EnumChoice.new('one', {'note' => 'something'})])
129
- # ENUM parses filled blocks: "enum filled {\none\ntwo}" =? DBML::Enum.new('filled', [DBML::EnumChoice.new('one', {}), DBML::EnumChoice.new('two', {})])
151
+ # ENUM parses settings: "enum setting {\none [note: 'something']\n}" => DBML::Enum.new('setting', [DBML::EnumChoice.new('one', {note: 'something'})])
152
+ # ENUM parses filled blocks: "enum filled {\none\ntwo}" => DBML::Enum.new('filled', [DBML::EnumChoice.new('one', {}), DBML::EnumChoice.new('two', {})])
130
153
 
131
- ENUM_CHOICE = seq_(/[^\{\}\s]+/.r, SETTINGS.maybe).map {|(name, settings)| EnumChoice.new name, unwrap(settings) }
132
- ENUM = block 'enum', /\S+/.r, ENUM_CHOICE do |(name, choices)|
154
+ ENUM_CHOICE = seq_(IDENTIFIER, SETTINGS.maybe).map {|(name, settings)| EnumChoice.new name, unwrap(settings) || {} }
155
+ ENUM = block 'enum', IDENTIFIER, ENUM_CHOICE do |(name, choices)|
133
156
  Enum.new name, choices
134
157
  end
135
158
 
@@ -153,16 +176,13 @@ module DBML
153
176
  # COLUMN parses naked identifiers as names: 'column_name type' => DBML::Column.new('column_name', 'type', {})
154
177
  # COLUMN parses quoted identifiers as names: '"column name" type' => DBML::Column.new('column name', 'type', {})
155
178
  # COLUMN parses types: 'name string' => DBML::Column.new('name', 'string', {})
156
- # COLUMN parses settings: 'name string [pk]' => DBML::Column.new('name', 'string', {'pk' => nil})
179
+ # COLUMN parses settings: 'name string [pk]' => DBML::Column.new('name', 'string', {pk: nil})
157
180
 
158
- QUOTED_COLUMN_NAME = '"'.r >> /[^"]+/.r << '"'.r
159
- UNQUOTED_COLUMN_NAME = /[^\{\}\s]+/.r
160
- COLUMN_TYPE = /\S+/.r
161
- COLUMN = seq_(
162
- QUOTED_COLUMN_NAME | UNQUOTED_COLUMN_NAME,
163
- COLUMN_TYPE,
164
- SETTINGS.maybe
165
- ) {|(name, type, settings)| Column.new name, type, unwrap(settings) || {} }
181
+ COLUMN_NAME = IDENTIFIER
182
+ COLUMN_TYPE = /[^\s\{\}]+/.r
183
+ COLUMN = seq_(COLUMN_NAME, COLUMN_TYPE, SETTINGS.maybe) do |(name, type, settings)|
184
+ Column.new name, type, unwrap(settings) || {}
185
+ end
166
186
 
167
187
  # Table Definition
168
188
  #
@@ -179,12 +199,12 @@ module DBML
179
199
  # TABLE parses empty tables: 'Table empty {}' => DBML::Table.new('empty', nil, [], [], [])
180
200
  # TABLE parses notes: "Table with_notes {\nNote: 'this is a note'\n}" => DBML::Table.new('with_notes', nil, ['this is a note'], [], [])
181
201
 
182
- TABLE_NAME = seq_(/[^\{\}\s]+/.r, ('as'.r >> /\S+/.r).maybe {|v| unwrap(v) })
202
+ TABLE_NAME = seq_(IDENTIFIER, ('as'.r >> IDENTIFIER).maybe {|v| unwrap(v) })
183
203
  TABLE = block 'Table', TABLE_NAME, (INDEXES | NOTE | COLUMN) do |((name, aliaz), objects)|
184
204
  Table.new name, aliaz,
185
205
  objects.select {|o| o.is_a? String },
186
206
  objects.select {|o| o.is_a? Column },
187
- objects.select {|o| (o.is_a? Array) && (o.all? {|e| e.is_a? Index })}.flatten
207
+ objects.select {|o| o.is_a? Index }
188
208
  end
189
209
 
190
210
  # TableGroup
@@ -200,10 +220,77 @@ module DBML
200
220
  #
201
221
  # TABLE_GROUP parses names: 'TableGroup group1 { }' => DBML::TableGroup.new('group1', [])
202
222
  # TABLE_GROUP parses tables: "TableGroup group2 {\ntable1\ntable2\n}" => DBML::TableGroup.new('group2', ['table1', 'table2'])
203
- TABLE_GROUP = block 'TableGroup', /\S+/.r, /[^\{\}\s]+/.r do |(name, tables)|
223
+ TABLE_GROUP = block 'TableGroup', IDENTIFIER, IDENTIFIER do |(name, tables)|
204
224
  TableGroup.new name, tables
205
225
  end
206
226
 
227
+ # Relationships & Foreign Key Definitions
228
+ #
229
+ # Relationships are used to define foreign key constraints between tables.
230
+ #
231
+ # Table posts {
232
+ # id integer [primary key]
233
+ # user_id integer [ref: > users.id] // many-to-one
234
+ # }
235
+ #
236
+ # // or this
237
+ # Table users {
238
+ # id integer [ref: < posts.user_id, ref: < reviews.user_id] // one to many
239
+ # }
240
+ #
241
+ # // The space after '<' is optional
242
+ #
243
+ # There are 3 types of relationships: one-to-one, one-to-many, and many-to-one
244
+ #
245
+ # 1. <: one-to-many. E.g: users.id < posts.user_id
246
+ # 2. >: many-to-one. E.g: posts.user_id > users.id
247
+ # 3. -: one-to-one. E.g: users.id - user_infos.user_id
248
+ #
249
+ # Composite foreign keys:
250
+ #
251
+ # Ref: merchant_periods.(merchant_id, country_code) > merchants.(id, country_code)
252
+ #
253
+ # In DBML, there are 3 syntaxes to define relationships:
254
+ #
255
+ # //Long form
256
+ # Ref name_optional {
257
+ # table1.column1 < table2.column2
258
+ # }
259
+ #
260
+ # //Short form:
261
+ # Ref name_optional: table1.column1 < table2.column2
262
+ #
263
+ # // Inline form
264
+ # Table posts {
265
+ # id integer
266
+ # user_id integer [ref: > users.id]
267
+ # }
268
+ #
269
+ # Relationship settings
270
+ #
271
+ # Ref: products.merchant_id > merchants.id [delete: cascade, update: no action]
272
+ #
273
+ # * delete / update: cascade | restrict | set null | set default | no action
274
+ # Define referential actions. Similar to ON DELETE/UPDATE CASCADE/... in SQL.
275
+ #
276
+ # Relationship settings are not supported for inline form ref.
277
+ #
278
+ # COMPOSITE_COLUMNS parses single column: '(column)' => ['column']
279
+ # COMPOSITE_COLUMNS parses multiple columns: '(col1, col2)' => ['col1', 'col2']
280
+ # RELATIONSHIP_PART parses simple form: 'table.column' => ['table', ['column']]
281
+ # RELATIONSHIP_PART parses composite form: 'table.(a, b)' => ['table', ['a', 'b']]
282
+ # RELATIONSHIP parses long form: "Ref name {\nleft.lcol < right.rcol\n}" => DBML::Relationship.new('name', 'left', ['lcol'], '<', 'right', ['rcol'], {})
283
+ # RELATIONSHIP parses short form: "Ref name: left.lcol > right.rcol" => DBML::Relationship.new('name', 'left', ['lcol'], '>', 'right', ['rcol'], {})
284
+ # RELATIONSHIP parses composite form: 'Ref: left.(a, b) - right.(c, d)' => DBML::Relationship.new(nil, 'left', ['a', 'b'], '-', 'right', ['c', 'd'], {})
285
+ # RELATIONSHIP parses settings: "Ref: L.a > R.b [delete: cascade, update: no action]" => DBML::Relationship.new(nil, 'L', ['a'], '>', 'R', ['b'], {delete: :cascade, update: :'no action'})
286
+ COMPOSITE_COLUMNS = '('.r >> comma_separated(COLUMN_NAME) << ')'
287
+ RELATIONSHIP_TYPE = '>'.r | '<'.r | '-'.r
288
+ RELATIONSHIP_PART = seq(seq(IDENTIFIER, '.'.r)[0], (COLUMN_NAME.map {|c| [c]}) | COMPOSITE_COLUMNS)
289
+ RELATIONSHIP_BODY = seq_(RELATIONSHIP_PART, RELATIONSHIP_TYPE, RELATIONSHIP_PART, SETTINGS.maybe)
290
+ RELATIONSHIP = seq_('Ref'.r >> NAKED_IDENTIFIER.maybe, long_or_short(RELATIONSHIP_BODY)).map do |(name, (left, type, right, settings))|
291
+ Relationship.new unwrap(name), *left, type, *right, unwrap(settings) || {}
292
+ end
293
+
207
294
  # Project Definition
208
295
  # ==================
209
296
  # You can give overall description of the project.
@@ -215,24 +302,25 @@ module DBML
215
302
  #
216
303
  # PROJECT_DEFINITION parses names: 'Project my_proj { }' => DBML::ProjectDef.new('my_proj', [], {})
217
304
  # PROJECT_DEFINITION parses notes: "Project my_porg { Note: 'porgs are cool!' }" => DBML::ProjectDef.new('my_porg', ['porgs are cool!'], {})
218
- # PROJECT_DEFINITION parses settings: "Project my_cool {\ndatabase_type: 'PostgreSQL'\n}" => DBML::ProjectDef.new('my_cool', [], {'database_type' => 'PostgreSQL'})
219
- PROJECT_DEFINITION = block 'Project', /\S+/.r, (NOTE | SETTING).star do |(name, objects)|
305
+ # PROJECT_DEFINITION parses settings: "Project my_cool {\ndatabase_type: 'PostgreSQL'\n}" => DBML::ProjectDef.new('my_cool', [], {database_type: 'PostgreSQL'})
306
+ PROJECT_DEFINITION = block 'Project', IDENTIFIER, (NOTE | SETTING).star do |(name, objects)|
220
307
  ProjectDef.new name,
221
308
  objects.select {|o| o.is_a? String },
222
309
  objects.select {|o| o.is_a? Hash }.reduce({}, &:update)
223
310
  end
224
311
 
225
- # PROJECT can be empty: "" => DBML::Project.new(nil, [], {}, [], [], [])
226
- # PROJECT includes definition info: "Project p { Note: 'hello' }" => DBML::Project.new('p', ['hello'], {}, [], [], [])
227
- # PROJECT includes tables: "Table t { }" => DBML::Project.new(nil, [], {}, [DBML::Table.new('t', nil, [], [], [])], [], [])
228
- # PROJECT includes enums: "enum E { }" => DBML::Project.new(nil, [], {}, [], [DBML::Enum.new('E', [])], [])
229
- # PROJECT includes table groups: "TableGroup TG { }" => DBML::Project.new(nil, [], {}, [], [], [DBML::TableGroup.new('TG', [])])
230
- PROJECT = space_surrounded(PROJECT_DEFINITION | TABLE | TABLE_GROUP | ENUM).star do |objects|
312
+ # PROJECT can be empty: "" => DBML::Project.new(nil, [], {}, [], [], [], [])
313
+ # PROJECT includes definition info: "Project p { Note: 'hello' }" => DBML::Project.new('p', ['hello'], {}, [], [], [], [])
314
+ # PROJECT includes tables: "Table t { }" => DBML::Project.new(nil, [], {}, [DBML::Table.new('t', nil, [], [], [])], [], [], [])
315
+ # PROJECT includes enums: "enum E { }" => DBML::Project.new(nil, [], {}, [], [], [DBML::Enum.new('E', [])], [])
316
+ # PROJECT includes table groups: "TableGroup TG { }" => DBML::Project.new(nil, [], {}, [], [], [], [DBML::TableGroup.new('TG', [])])
317
+ PROJECT = space_surrounded(PROJECT_DEFINITION | RELATIONSHIP | TABLE | TABLE_GROUP | ENUM).star do |objects|
231
318
  definition = objects.find {|o| o.is_a? ProjectDef }
232
319
  Project.new definition.nil? ? nil : definition.name,
233
320
  definition.nil? ? [] : definition.notes,
234
321
  definition.nil? ? {} : definition.settings,
235
322
  objects.select {|o| o.is_a? Table },
323
+ objects.select {|o| o.is_a? Relationship },
236
324
  objects.select {|o| o.is_a? Enum },
237
325
  objects.select {|o| o.is_a? TableGroup }
238
326
  end
@@ -1,3 +1,3 @@
1
1
  module DBML
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Worthington
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-13 00:00:00.000000000 Z
11
+ date: 2020-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rsec