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.
- 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
|