Skip to content

Commit

Permalink
Rewrite SQL and DBML rendering (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vanderhoof authored Jul 18, 2024
1 parent 6e0af33 commit 90c63c8
Show file tree
Hide file tree
Showing 95 changed files with 2,895 additions and 2,553 deletions.
5 changes: 1 addition & 4 deletions pydbml/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from . import classes
from . import _classes
from .parser import PyDBML
from .database import Database
from pydbml.constants import MANY_TO_ONE
from pydbml.constants import ONE_TO_MANY
from pydbml.constants import ONE_TO_ONE
Empty file added pydbml/_classes/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions pydbml/classes/base.py → pydbml/_classes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ def check_attributes_for_sql(self):
raise AttributeMissingError(
f'Cannot render SQL. Missing required attribute "{attr}".'
)
@property
def sql(self) -> str:
if hasattr(self, 'database') and self.database is not None:
renderer = self.database.sql_renderer
else:
from pydbml.renderer.sql.default import DefaultSQLRenderer
renderer = DefaultSQLRenderer

return renderer.render(self)

def __setattr__(self, name: str, value: Any):
"""
Expand All @@ -46,3 +55,16 @@ def __eq__(self, other: object) -> bool:
other_dict.pop(field, None)

return self_dict == other_dict


class DBMLObject:
'''Base class for all DBML objects.'''
@property
def dbml(self) -> str:
if hasattr(self, 'database') and self.database is not None:
renderer = self.database.dbml_renderer
else:
from pydbml.renderer.dbml.default import DefaultDBMLRenderer
renderer = DefaultDBMLRenderer

return renderer.render(self)
92 changes: 92 additions & 0 deletions pydbml/_classes/column.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union

from pydbml.exceptions import TableNotFoundError
from .base import SQLObject, DBMLObject
from .enum import Enum
from .expression import Expression
from .note import Note

if TYPE_CHECKING: # pragma: no cover
from .table import Table
from .reference import Reference


class Column(SQLObject, DBMLObject):
'''Class representing table column.'''

required_attributes = ('name', 'type')
dont_compare_fields = ('table',)

def __init__(self,
name: str,
type: Union[str, Enum],
unique: bool = False,
not_null: bool = False,
pk: bool = False,
autoinc: bool = False,
default: Optional[Union[str, int, bool, float, Expression]] = None,
note: Optional[Union[Note, str]] = None,
# ref_blueprints: Optional[List[ReferenceBlueprint]] = None,
comment: Optional[str] = None):
self.name = name
self.type = type
self.unique = unique
self.not_null = not_null
self.pk = pk
self.autoinc = autoinc
self.comment = comment
self.note = Note(note)

self.default = default
self.table: Optional['Table'] = None

def __eq__(self, other: object) -> bool:
if other is self:
return True
if not isinstance(other, self.__class__):
return False
self_table = self.table.full_name if self.table else None
other_table = other.table.full_name if other.table else None
if self_table != other_table:
return False
return super().__eq__(other)

@property
def note(self):
return self._note

@note.setter
def note(self, val: Note) -> None:
self._note = val
val.parent = self

def get_refs(self) -> List['Reference']:
'''
get all references related to this column (where this col is col1 in)
'''
if not self.table:
raise TableNotFoundError('Table for the column is not set')
return [ref for ref in self.table.get_refs() if self in ref.col1]

@property
def database(self):
return self.table.database if self.table else None

def __repr__(self):
'''
>>> Column('name', 'VARCHAR2')
<Column 'name', 'VARCHAR2'>
'''
type_name = self.type if isinstance(self.type, str) else self.type.name
return f'<Column {self.name!r}, {type_name!r}>'

def __str__(self):
'''
>>> print(Column('name', 'VARCHAR2'))
name[VARCHAR2]
'''

return f'{self.name}[{self.type}]'
75 changes: 8 additions & 67 deletions pydbml/classes/enum.py → pydbml/_classes/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@
from typing import Optional
from typing import Union

from .base import SQLObject
from .base import SQLObject, DBMLObject
from .note import Note
from pydbml.tools import comment_to_dbml
from pydbml.tools import comment_to_sql
from pydbml.tools import indent
from pydbml.tools import note_option_to_dbml


class EnumItem:
class EnumItem(SQLObject, DBMLObject):
'''Single enum item'''

required_attributes = ('name',)

def __init__(self,
name: str,
note: Optional[Union['Note', str]] = None,
note: Optional[Union[Note, str]] = None,
comment: Optional[str] = None):
self.name = name
self.note = Note(note)
Expand All @@ -32,37 +30,15 @@ def note(self, val: Note) -> None:
val.parent = self

def __repr__(self):
'''
>>> EnumItem('en-US')
<EnumItem 'en-US'>
'''

'''<EnumItem 'en-US'>'''
return f'<EnumItem {self.name!r}>'

def __str__(self):
'''
>>> print(EnumItem('en-US'))
en-US
'''

'''en-US'''
return self.name

@property
def sql(self):
result = comment_to_sql(self.comment) if self.comment else ''
result += f"'{self.name}',"
return result

@property
def dbml(self):
result = comment_to_dbml(self.comment) if self.comment else ''
result += f'"{self.name}"'
if self.note:
result += f' [{note_option_to_dbml(self.note)}]'
return result


class Enum(SQLObject):
class Enum(SQLObject, DBMLObject):
required_attributes = ('name', 'schema', 'items')

def __init__(self,
Expand Down Expand Up @@ -111,38 +87,3 @@ def __str__(self):
'''

return self.name

def _get_full_name_for_sql(self) -> str:
if self.schema == 'public':
return f'"{self.name}"'
else:
return f'"{self.schema}"."{self.name}"'

@property
def sql(self):
'''
Returns SQL for enum type:
CREATE TYPE "job_status" AS ENUM (
'created',
'running',
'donef',
'failure',
);
'''
self.check_attributes_for_sql()
result = comment_to_sql(self.comment) if self.comment else ''
result += f'CREATE TYPE {self._get_full_name_for_sql()} AS ENUM (\n'
result += '\n'.join(f'{indent(i.sql, 2)}' for i in self.items)
result += '\n);'
return result

@property
def dbml(self):
result = comment_to_dbml(self.comment) if self.comment else ''
result += f'Enum {self._get_full_name_for_sql()} {{\n'
items_str = '\n'.join(i.dbml for i in self.items)
result += indent(items_str)
result += '\n}'
return result
12 changes: 2 additions & 10 deletions pydbml/classes/expression.py → pydbml/_classes/expression.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .base import SQLObject
from .base import SQLObject, DBMLObject


class Expression(SQLObject):
class Expression(SQLObject, DBMLObject):
def __init__(self, text: str):
self.text = text

Expand All @@ -20,11 +20,3 @@ def __repr__(self) -> str:
'''

return f'Expression({repr(self.text)})'

@property
def sql(self) -> str:
return f'({self.text})'

@property
def dbml(self) -> str:
return f'`{self.text}`'
70 changes: 70 additions & 0 deletions pydbml/_classes/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from typing import List
from typing import Literal
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union

from .base import SQLObject, DBMLObject
from .column import Column
from .expression import Expression
from .note import Note

if TYPE_CHECKING: # pragma: no cover
from .table import Table


class Index(SQLObject, DBMLObject):
'''Class representing index.'''
required_attributes = ('subjects', 'table')
dont_compare_fields = ('table',)

def __init__(self,
subjects: List[Union[str, Column, Expression]],
name: Optional[str] = None,
unique: bool = False,
type: Optional[Literal['hash', 'btree']] = None,
pk: bool = False,
note: Optional[Union[Note, str]] = None,
comment: Optional[str] = None):
self.subjects = subjects
self.table: Optional[Table] = None

self.name = name if name else None
self.unique = unique
self.type = type
self.pk = pk
self.note = Note(note)
self.comment = comment

@property
def note(self):
return self._note

@note.setter
def note(self, val: Note) -> None:
self._note = val
val.parent = self

@property
def subject_names(self):
'''
Returns updated list of subject names.
'''
return [s.name if isinstance(s, Column) else str(s) for s in self.subjects]

def __repr__(self):
'''
<Index 'test', ['col', '(c*2)']>
'''

table_name = self.table.name if self.table else None
return f"<Index {table_name!r}, {self.subject_names!r}>"

def __str__(self):
'''
Index(test[col, (c*2)])
'''

table_name = self.table.name if self.table else ''
subjects = ', '.join(self.subject_names)
return f"Index({table_name}[{subjects}])"
23 changes: 23 additions & 0 deletions pydbml/_classes/note.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Any

from .base import SQLObject, DBMLObject


class Note(SQLObject, DBMLObject):
dont_compare_fields = ('parent',)

def __init__(self, text: Any) -> None:
self.text: str
self.text = str(text) if text is not None else ''
self.parent: Any = None

def __str__(self):
'''Note text'''
return self.text

def __bool__(self):
return bool(self.text)

def __repr__(self):
'''Note('Note text')'''
return f'Note({repr(self.text)})'
34 changes: 34 additions & 0 deletions pydbml/_classes/project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Dict
from typing import Optional
from typing import Union

from pydbml._classes.base import DBMLObject
from pydbml._classes.note import Note


class Project(DBMLObject):
dont_compare_fields = ('database',)

def __init__(self,
name: str,
items: Optional[Dict[str, str]] = None,
note: Optional[Union[Note, str]] = None,
comment: Optional[str] = None):
self.database = None
self.name = name
self.items = items
self.note = Note(note)
self.comment = comment

def __repr__(self):
"""<Project 'myproject'>"""
return f'<Project {self.name!r}>'

@property
def note(self):
return self._note

@note.setter
def note(self, val: Note) -> None:
self._note = val
val.parent = self
Loading

0 comments on commit 90c63c8

Please sign in to comment.