dbml 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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