Skip to content

Commit

Permalink
Merge pull request #42 from Vanderhoof/develop
Browse files Browse the repository at this point in the history
Develop into master
  • Loading branch information
Vanderhoof authored Jul 25, 2024
2 parents 6e0af33 + 2e5e32c commit 34d8889
Show file tree
Hide file tree
Showing 105 changed files with 3,308 additions and 2,743 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 1.1.0

- New: SQL and DBML rendering rewritten tow support external renderers
- New: allow unicode characters in identifiers (DBML v3.3.0)
- New: support for arbitrary table and column properties (#37)

# 1.0.11
- Fix: allow pk in named indexes (thanks @pierresouchay for the contribution)

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# DBML parser for Python

*Compliant with DBML **v3.2.0** syntax*
*Compliant with DBML **v3.6.1** syntax*

PyDBML is a Python parser and builder for [DBML](https://www.dbml.org) syntax.

Expand All @@ -13,6 +13,7 @@ PyDBML is a Python parser and builder for [DBML](https://www.dbml.org) syntax.
* [Class Reference](docs/classes.md)
* [Creating DBML schema](docs/creating_schema.md)
* [Upgrading to PyDBML 1.0.0](docs/upgrading.md)
* [Arbitrary Properties](docs/properties.md)

> PyDBML requires Python v3.8 or higher
Expand Down
77 changes: 77 additions & 0 deletions docs/properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Arbitrary Properties

Since 1.1.0 PyDBML supports arbitrary properties in Table and Column definitions. Arbitrary properties is a dictionary of key-value pairs that can be added to any Table or Column manually, or parsed from a DBML file. This may be useful for extending the standard DBML syntax or keeping additional information in the schema.

Arbitrary properties are turned off by default. To enable parsing properties in DBML files, set `allow_properties` argument to `True` in the parser call. To enable rendering properties in the output DBML of an existing database, set `allow_properties` database attribute to `True`.

## Properties in DBML

In a DBML file arbitrary properties are defined like this:

```python
>>> dbml_str = '''
... Table "products" {
... "id" integer
... "name" varchar [col_prop: 'some value']
... table_prop: 'another value'
... }'''

```

In this example we've added a property `col_prop` to the column `name` and a property `table_prop` to the table `products`. Note that property values must me single-quoted strings. Multiline strings (with `'''`) are supported.

Now let's parse this DBML string:

```python
>>> from pydbml import PyDBML
>>> mydb = PyDBML(dbml_str, allow_properties=True)
>>> mydb.tables[0].columns[1].properties
{'col_prop': 'some value'}
>>> mydb.tables[0].properties
{'table_prop': 'another value'}

```

The `allow_properties=True` argument is crucial here. Without it, the parser will raise syntax errors.

## Rendering Properties

To render properties in the output DBML, set `allow_properties` attribute of the Database object to `True`. If you parsed the DBML with `allow_properties=True`, the result database will already have this attribute set to `True`.

We will reuse the `mydb` database from the previous example:

```python
>>> print(mydb.allow_properties)
True

```

Let's set a new property on the table and render the DBML:

```python
>>> mydb.tables[0].properties['new_prop'] = 'Multiline\nproperty\nvalue'
>>> print(mydb.dbml)
Table "products" {
"id" integer
"name" varchar [col_prop: 'some value']
<BLANKLINE>
table_prop: 'another value'
new_prop: '''
Multiline
property
value'''
}

```

As you see, properties are also rendered in the output DBML correctly. But if `allow_properties` is set to `False`, the properties will be ignored:

```python
>>> mydb.allow_properties = False
>>> print(mydb.dbml)
Table "products" {
"id" integer
"name" varchar
}

```
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)
94 changes: 94 additions & 0 deletions pydbml/_classes/column.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from typing import List, Dict
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,
comment: Optional[str] = None,
properties: Union[Dict[str, str], None] = 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.properties = properties if properties else {}

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}`'
Loading

0 comments on commit 34d8889

Please sign in to comment.