-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathblueprints.py
More file actions
327 lines (283 loc) · 9.38 KB
/
blueprints.py
File metadata and controls
327 lines (283 loc) · 9.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
from dataclasses import dataclass
from typing import Any
from typing import Collection
from typing import Dict
from typing import List
from typing import Literal
from typing import Optional
from typing import Union
from pydbml.classes import (
Column,
Enum,
EnumItem,
Expression,
Index,
Note,
Project,
Reference,
Table,
TableGroup
)
from pydbml._classes.sticky_note import StickyNote
from pydbml.exceptions import ColumnNotFoundError, TableNotFoundError, ValidationError
from pydbml.tools import remove_indentation, strip_empty_lines
class Blueprint:
parser = None
@dataclass
class NoteBlueprint(Blueprint):
text: str
def _preformat_text(self) -> str:
'''Preformat the note text for idempotence'''
result = strip_empty_lines(self.text)
result = remove_indentation(result)
return result
def build(self) -> 'Note':
text = self._preformat_text()
return Note(text)
@dataclass
class StickyNoteBlueprint(Blueprint):
name: str
text: str
def _preformat_text(self) -> str:
'''Preformat the note text for idempotence'''
result = strip_empty_lines(self.text)
result = remove_indentation(result)
return result
def build(self) -> StickyNote:
text = self._preformat_text()
name = self.name
return StickyNote(name=name, text=text)
@dataclass
class ExpressionBlueprint(Blueprint):
text: str
def build(self) -> Expression:
return Expression(self.text)
@dataclass
class ReferenceBlueprint(Blueprint):
type: Literal['>', '<', '-', '<>']
inline: bool
name: Optional[str] = None
schema1: str = 'public'
table1: Optional[str] = None
col1: Optional[Union[str, Collection[str]]] = None
schema2: str = 'public'
table2: Optional[str] = None
col2: Optional[Union[str, Collection[str]]] = None
comment: Optional[str] = None
on_update: Optional[str] = None
on_delete: Optional[str] = None
def build(self) -> 'Reference':
'''
both tables and columns should be present before build
'''
if self.table1 is None:
raise TableNotFoundError("Can't build Reference, table1 unknown")
if self.table2 is None:
raise TableNotFoundError("Can't build Reference, table2 unknown")
if self.col1 is None:
raise ColumnNotFoundError("Can't build Reference, col1 unknown")
if self.col2 is None:
raise ColumnNotFoundError("Can't build Reference, col2 unknown")
if self.parser:
table1 = self.parser.locate_table(self.schema1, self.table1)
else:
raise RuntimeError('Parser is not set')
col1_list = [c.strip('() ') for c in self.col1.split(',')]
col1 = [table1[col] for col in col1_list]
table2 = self.parser.locate_table(self.schema2, self.table2)
col2_list = [c.strip('() ') for c in self.col2.split(',')]
col2 = [table2[col] for col in col2_list]
return Reference(
type=self.type,
inline=self.inline,
col1=col1,
col2=col2,
name=self.name,
comment=self.comment,
on_update=self.on_update,
on_delete=self.on_delete
)
@dataclass
class ColumnBlueprint(Blueprint):
name: str
type: str
unique: bool = False
not_null: bool = False
pk: bool = False
autoinc: bool = False
default: Optional[Any] = None
note: Optional[NoteBlueprint] = None
ref_blueprints: Optional[List[ReferenceBlueprint]] = None
comment: Optional[str] = None
properties: Optional[Dict[str, str]] = None
def build(self) -> 'Column':
if isinstance(self.default, ExpressionBlueprint):
self.default = self.default.build()
if self.parser:
if '.' in self.type:
schema, name = self.type.split('.')
else:
schema, name = 'public', self.type
for enum in self.parser.database.enums:
if (enum.schema, enum.name) == (schema, name):
self.type = enum
break
return Column(
name=self.name,
type=self.type,
unique=self.unique,
not_null=self.not_null,
pk=self.pk,
autoinc=self.autoinc,
default=self.default,
note=self.note.build() if self.note else None,
comment=self.comment,
properties=self.properties,
)
@dataclass
class IndexBlueprint(Blueprint):
subject_names: List[Union[str, ExpressionBlueprint]]
name: Optional[str] = None
unique: bool = False
type: Optional[
Literal[
# https://www.postgresql.org/docs/current/indexes-types.html
"brin",
"btree",
"gin",
"gist",
"hash",
"spgist",
]
] = None
pk: bool = False
note: Optional[NoteBlueprint] = None
comment: Optional[str] = None
table = None
def build(self) -> 'Index':
return Index(
# TableBlueprint will process subjects
subjects=[],
name=self.name,
unique=self.unique,
type=self.type,
pk=self.pk,
note=self.note.build() if self.note else None,
comment=self.comment
)
@dataclass
class TableBlueprint(Blueprint):
name: str
schema: str = 'public'
columns: Optional[List[ColumnBlueprint]] = None
indexes: Optional[List[IndexBlueprint]] = None
alias: Optional[str] = None
note: Optional[NoteBlueprint] = None
header_color: Optional[str] = None
comment: Optional[str] = None
properties: Optional[Dict[str, str]] = None
def build(self) -> 'Table':
result = Table(
name=self.name,
schema=self.schema,
alias=self.alias,
note=self.note.build() if self.note else None,
header_color=self.header_color,
comment=self.comment,
properties=self.properties
)
columns = self.columns or []
indexes = self.indexes or []
for col_bp in columns:
result.add_column(col_bp.build())
for index_bp in indexes:
index = index_bp.build()
new_subjects: List[Union[str, Column, Expression]] = []
for subj in index_bp.subject_names:
if isinstance(subj, ExpressionBlueprint):
new_subjects.append(subj.build())
else:
for col in result.columns:
if col.name == subj:
new_subjects.append(col)
break
else:
raise ColumnNotFoundError(
f'Cannot add index, column "{subj}" not defined in'
' table "{self.name}".'
)
index.subjects = new_subjects
result.add_index(index)
return result
def get_reference_blueprints(self):
''' the inline ones '''
result = []
for col in self.columns:
for ref_bp in col.ref_blueprints or []:
ref_bp.schema1 = self.schema
ref_bp.table1 = self.name
ref_bp.col1 = col.name
result.append(ref_bp)
return result
@dataclass
class EnumItemBlueprint(Blueprint):
name: str
note: Optional[NoteBlueprint] = None
comment: Optional[str] = None
def build(self) -> 'EnumItem':
return EnumItem(
name=self.name,
note=self.note.build() if self.note else None,
comment=self.comment
)
@dataclass
class EnumBlueprint(Blueprint):
name: str
items: List[EnumItemBlueprint]
schema: str = 'public'
comment: Optional[str] = None
def build(self) -> 'Enum':
return Enum(
name=self.name,
items=[ei.build() for ei in self.items],
schema=self.schema,
comment=self.comment
)
@dataclass
class ProjectBlueprint(Blueprint):
name: str
items: Optional[Dict[str, str]] = None
note: Optional[NoteBlueprint] = None
comment: Optional[str] = None
def build(self) -> 'Project':
return Project(
name=self.name,
items=dict(self.items) if self.items else {},
note=self.note.build() if self.note else None,
comment=self.comment
)
@dataclass
class TableGroupBlueprint(Blueprint):
name: str
items: List[str]
comment: Optional[str] = None
note: Optional[NoteBlueprint] = None
color: Optional[str] = None
def build(self) -> 'TableGroup':
if not self.parser:
raise RuntimeError('Parser is not set')
items = []
for table_name in self.items:
components = table_name.split('.')
schema, table = components if len(components) == 2 else ('public', components[0])
table_obj = self.parser.locate_table(schema, table)
if table_obj in items:
raise ValidationError(f'Table "{table}" is already in group "{self.name}"')
items.append(table_obj)
return TableGroup(
name=self.name,
items=items,
comment=self.comment,
note=self.note.build() if self.note else None,
color=self.color
)