dbml 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/dbml.rb +132 -44
- data/lib/dbml/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48c1cc0879db898fd838bccb388775692c2fd9510cc889dfb06aea5bc6532856
|
4
|
+
data.tar.gz: a6aa38aa4625fad6f0dafc7a49e1866f6376d222cf92dede1ea0994583312388
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4ec98ffd66c2c197329a74a44bbecf8766124a12fe1e809b121d6a9428e7378e6a65a4c7e93c62f8a94b9662f69da5a1985010e6452d1cf945fb49befcde6ac5
|
7
|
+
data.tar.gz: 2c1c230ed637641c8fc798cf04dbc21eb282bfbb428ae6fdeeb752602dc22fcab549712e208f2b52d0e7f119068fb49ab7d2b26d8a1b66e50cb18ed491fa2517
|
data/lib/dbml.rb
CHANGED
@@ -1,18 +1,20 @@
|
|
1
1
|
require 'rsec'
|
2
|
-
include Rsec::Helpers
|
3
2
|
|
4
3
|
module DBML
|
5
|
-
Column
|
6
|
-
Table
|
7
|
-
Index
|
8
|
-
Expression
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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]' => {
|
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
|
-
|
67
|
-
SETTINGS
|
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
|
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 =
|
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', {
|
129
|
-
# ENUM parses filled blocks: "enum filled {\none\ntwo}"
|
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_(
|
132
|
-
ENUM = block 'enum',
|
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', {
|
179
|
+
# COLUMN parses settings: 'name string [pk]' => DBML::Column.new('name', 'string', {pk: nil})
|
157
180
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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_(
|
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|
|
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',
|
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', [], {
|
219
|
-
PROJECT_DEFINITION = block 'Project',
|
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
|
data/lib/dbml/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2020-09-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rsec
|