Skip to content

Commit

Permalink
Merge pull request #33 from Vanderhoof/feat/catch_up_syntax
Browse files Browse the repository at this point in the history
Feat/catch up syntax
  • Loading branch information
Vanderhoof authored Mar 17, 2024
2 parents 952fd07 + 4f21e39 commit 872be6d
Show file tree
Hide file tree
Showing 31 changed files with 384 additions and 29 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# 1.0.10
- New: Sticky notes syntax (DBML v3.2.0)
- Fix: Table header color was not rendered in `dbml()` (thanks @tristangrebot for the contribution)
- New: allow array column types (DBML v3.1.0)
- New: allow double quotes in expressions (DBML v3.1.2)
- Fix: recursion in object equality check
- New: don't allow duplicate refs even if they have different inline method (DBML v3.1.6)

# 1.0.9

- Fix: enum collision from different schemas. Thanks @ewdurbin for the contribution
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022
Copyright (c) 2024

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
4 changes: 2 additions & 2 deletions 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 **v2.6.1** syntax*
*Compliant with DBML **v3.2.0** syntax*

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

Expand Down Expand Up @@ -138,7 +138,7 @@ Enum "product status" {
"In Stock"
}
<BLANKLINE>
Table "orders" {
Table "orders" [headercolor: #fff] {
"id" int [pk, increment]
"user_id" int [unique, not null]
"status" "orders_status"
Expand Down
1 change: 0 additions & 1 deletion TODO.md

This file was deleted.

13 changes: 13 additions & 0 deletions docs/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [Reference](#reference)
* [Enum](#enum)
* [Note](#note)
* [StickyNote](#sticky_note)
* [Expression](#expression)
* [Project](#project)
* [TableGroup](#tablegroup)
Expand Down Expand Up @@ -260,6 +261,18 @@ Note is a basic class, which may appear in some other classes' `note` attribute.
* **sql** (str) — SQL definition for this note.
* **dbml** (str) — DBML definition for this note.

## Note

**new in PyDBML 1.0.10**

Sticky notes are similar to regular notes, except that they are defined at the root of your DBML file and have a name.

### Attributes

**name** (str) — note name.
**text** (str) — note text.
* **dbml** (str) — DBML definition for this note.

## Expression

**new in PyDBML 1.0.0**
Expand Down
16 changes: 13 additions & 3 deletions pydbml/classes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class SQLObject:
Base class for all SQL objects.
'''
required_attributes: Tuple[str, ...] = ()
dont_compare_fields: Tuple[str, ...] = ()

def check_attributes_for_sql(self):
'''
Expand All @@ -33,6 +34,15 @@ def __eq__(self, other: object) -> bool:
attributes are equal.
"""

if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
return False
if not isinstance(other, self.__class__):
return False
# not comparing those because they are circular references

self_dict = dict(self.__dict__)
other_dict = dict(other.__dict__)

for field in self.dont_compare_fields:
self_dict.pop(field, None)
other_dict.pop(field, None)

return self_dict == other_dict
10 changes: 10 additions & 0 deletions pydbml/classes/column.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Column(SQLObject):
'''Class representing table column.'''

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

def __init__(self,
name: str,
Expand All @@ -45,6 +46,15 @@ def __init__(self,
self.default = default
self.table: Optional['Table'] = None

def __eq__(self, other: object) -> bool:
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
Expand Down
1 change: 1 addition & 0 deletions pydbml/classes/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
class Index(SQLObject):
'''Class representing index.'''
required_attributes = ('subjects', 'table')
dont_compare_fields = ('table',)

def __init__(self,
subjects: List[Union[str, 'Column', 'Expression']],
Expand Down
10 changes: 4 additions & 6 deletions pydbml/classes/note.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import re
from typing import Any
from typing import Any, Union

from .base import SQLObject
from pydbml.tools import indent
from pydbml import classes


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

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

def __str__(self):
Expand Down
2 changes: 2 additions & 0 deletions pydbml/classes/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@


class Project:
dont_compare_fields = ('database',)

def __init__(self,
name: str,
items: Optional[Dict[str, str]] = None,
Expand Down
1 change: 1 addition & 0 deletions pydbml/classes/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Reference(SQLObject):
and its `sql` property contains the ALTER TABLE clause.
'''
required_attributes = ('type', 'col1', 'col2')
dont_compare_fields = ('database', '_inline')

def __init__(self,
type: Literal['>', '<', '-', '<>'],
Expand Down
50 changes: 50 additions & 0 deletions pydbml/classes/sticky_note.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import re
from typing import Any

from pydbml.tools import indent


class StickyNote:
dont_compare_fields = ('database',)

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

self.database = None

def __str__(self):
'''
>>> print(StickyNote('mynote', 'Note text'))
StickyNote('mynote', 'Note text')
'''

return self.__class__.__name__ + f'({repr(self.name)}, {repr(self.text)})'

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

def __repr__(self):
'''
>>> StickyNote('mynote', 'Note text')
<StickyNote 'mynote', 'Note text'>
'''

return f'<{self.__class__.__name__} {self.name!r}, {self.text!r}>'

def _prepare_text_for_dbml(self):
'''Escape single quotes'''
pattern = re.compile(r"('''|')")
return pattern.sub(r'\\\1', self.text)

@property
def dbml(self):
text = self._prepare_text_for_dbml()
if '\n' in text:
note_text = f"'''\n{text}\n'''"
else:
note_text = f"'{text}'"

note_text = indent(note_text)
result = f'Note {self.name} {{\n{note_text}\n}}'
return result
1 change: 1 addition & 0 deletions pydbml/classes/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Table(SQLObject):
'''Class representing table.'''

required_attributes = ('name', 'schema')
dont_compare_fields = ('database',)

def __init__(self,
name: str,
Expand Down
1 change: 1 addition & 0 deletions pydbml/classes/table_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class TableGroup:
but after parsing the whole document, PyDBMLParseResults class replaces
them with references to actual tables.
'''
dont_compare_fields = ('database',)

def __init__(self,
name: str,
Expand Down
13 changes: 11 additions & 2 deletions pydbml/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from typing import Optional
from typing import Union

from .classes import Enum
from .classes import Enum, Note
from .classes import Project
from .classes import Reference
from .classes import Table
from .classes import TableGroup
from .classes.sticky_note import StickyNote
from .exceptions import DatabaseValidationError

from .constants import MANY_TO_ONE, ONE_TO_MANY
Expand Down Expand Up @@ -41,6 +42,7 @@ def __init__(self) -> None:
self.refs: List['Reference'] = []
self.enums: List['Enum'] = []
self.table_groups: List['TableGroup'] = []
self.sticky_notes: List['StickyNote'] = []
self.project: Optional['Project'] = None

def __repr__(self) -> str:
Expand Down Expand Up @@ -79,6 +81,8 @@ def add(self, obj: Any) -> Any:
return self.add_table_group(obj)
elif isinstance(obj, Project):
return self.add_project(obj)
elif isinstance(obj, StickyNote):
return self.add_sticky_note(obj)
else:
raise DatabaseValidationError(f'Unsupported type {type(obj)}.')

Expand Down Expand Up @@ -125,6 +129,11 @@ def add_enum(self, obj: Enum) -> Enum:
self.enums.append(obj)
return obj

def add_sticky_note(self, obj: StickyNote) -> StickyNote:
self._set_database(obj)
self.sticky_notes.append(obj)
return obj

def add_table_group(self, obj: TableGroup) -> TableGroup:
if obj in self.table_groups:
raise DatabaseValidationError(f'{obj} is already in the database.')
Expand Down Expand Up @@ -216,7 +225,7 @@ def dbml(self):
'''Generates DBML code out of parsed results'''
items = [self.project] if self.project else []
refs = (ref for ref in self.refs if not ref.inline)
items.extend((*self.enums, *self.tables, *refs, *self.table_groups))
items.extend((*self.enums, *self.tables, *refs, *self.table_groups, *self.sticky_notes))
components = (
i.dbml for i in items
)
Expand Down
2 changes: 1 addition & 1 deletion pydbml/definitions/column.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
type_args = ("(" + pp.original_text_for(expression) + ")")

# column type is parsed as a single string, it will be split by blueprint
column_type = pp.Combine((name + '.' + name) | ((name) + type_args[0, 1]))
column_type = pp.Combine((name + pp.Literal('[]')) | (name + '.' + name) | ((name) + type_args[0, 1]))

default = pp.CaselessLiteral('default:').suppress() + _ - (
string_literal
Expand Down
4 changes: 2 additions & 2 deletions pydbml/definitions/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@

# Expression

expr_chars = pp.Word(pp.alphanums + "'`,._+- \n\t")
expr_chars_no_comma_space = pp.Word(pp.alphanums + "'`._+-")
expr_chars = pp.Word(pp.alphanums + "\"'`,._+- \n\t")
expr_chars_no_comma_space = pp.Word(pp.alphanums + "\"'`._+-")
expression = pp.Forward()
factor = (
pp.Word(pp.alphanums + '_')[0, 1] + '(' + expression + ')'
Expand Down
21 changes: 21 additions & 0 deletions pydbml/definitions/sticky_note.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pyparsing as pp

from .common import _, end, _c
from .generic import string_literal, name
from ..parser.blueprints import StickyNoteBlueprint

sticky_note = _c + pp.CaselessLiteral('note') + _ + (name('name') + _ - '{' + _ - string_literal('text') + _ - '}') + end


def parse_sticky_note(s, loc, tok):
'''
Note single_line_note {
'This is a single line note'
}
'''
init_dict = {'name': tok['name'], 'text': tok['text']}

return StickyNoteBlueprint(**init_dict)


sticky_note.set_parse_action(parse_sticky_note)
4 changes: 4 additions & 0 deletions pydbml/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ class DBMLError(Exception):

class DatabaseValidationError(Exception):
pass


class ValidationError(Exception):
pass
24 changes: 23 additions & 1 deletion pydbml/parser/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
from pydbml.classes import Reference
from pydbml.classes import Table
from pydbml.classes import TableGroup
from pydbml.classes.sticky_note import StickyNote
from pydbml.exceptions import ColumnNotFoundError
from pydbml.exceptions import TableNotFoundError
from pydbml.exceptions import ValidationError
from pydbml.tools import remove_indentation
from pydbml.tools import strip_empty_lines

Expand All @@ -42,6 +44,23 @@ def build(self) -> 'Note':
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
Expand Down Expand Up @@ -280,7 +299,10 @@ def build(self) -> 'TableGroup':
for table_name in self.items:
components = table_name.split('.')
schema, table = components if len(components) == 2 else ('public', components[0])
items.append(self.parser.locate_table(schema, table))
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,
Expand Down
Loading

0 comments on commit 872be6d

Please sign in to comment.