Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added multi column support for postgres #78

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# IPython Notebook
*.ipynb
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# dotenv
.env

# virtualenv
venv/
venv3.7/
ENV/

# mock event
*.event

# Spyder project settings
.spyderproject

# Rope project settings
.ropeproject

#IDE specific
.idea/
.vscode/

#eclipse project
.project
.pydevproject

.DS_Store
7 changes: 5 additions & 2 deletions architect/databases/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ def __init__(self, model, **meta):
self.model = model
self.database = model.architect.operation
self.table = meta['table']
self.column_value = meta['column_value']
self.column_name = meta['column']
self.column_values = meta['column_values']
self.columns = meta['columns']
# backward compatibility
self.column_value = meta['column_values'][0]
self.column_name = meta['columns'][0]
self.pks = meta['pk'] if isinstance(meta['pk'], list) else [meta['pk']]

def prepare(self):
Expand Down
174 changes: 117 additions & 57 deletions architect/databases/postgresql/partition.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ def prepare(self):
"""
Prepares needed triggers and functions for those triggers.
"""
indentation = {'declarations': 5, 'variables': 6}
command_str = self._get_command_str()
return self.database.execute(command_str)

def _get_command_str(self):
indentation = {'declarations': 5, 'variables': 5}
definitions, formatters = self._get_definitions()

for definition in indentation:
Expand All @@ -28,22 +32,17 @@ def prepare(self):

definitions[definition] = '\n'.join(definitions[definition]).format(**formatters)

return self.database.execute("""
return """
-- We need to create a before insert function
CREATE OR REPLACE FUNCTION {{parent_table}}_insert_child()
RETURNS TRIGGER AS $$
DECLARE
match "{{parent_table}}".{{column}}%TYPE;
{declarations}
tablename VARCHAR;
checks TEXT;
{declarations}

BEGIN
IF NEW.{{column}} IS NULL THEN
tablename := '{{parent_table}}_null';
checks := '{{column}} IS NULL';
ELSE
{variables}
END IF;
{variables}

BEGIN
EXECUTE 'CREATE TABLE IF NOT EXISTS ' || tablename || ' (
Expand Down Expand Up @@ -100,8 +99,12 @@ def prepare(self):
""".format(**definitions).format(
pk=' AND '.join('{pk} = NEW.{pk}'.format(pk=pk) for pk in self.pks),
parent_table=self.table,
column='"{0}"'.format(self.column_name)
))
**{
'column_{idx}'.format(idx=idx): '"{0}"'.format(column) for idx, column in enumerate(
self.columns
)
}
)

def exists(self):
"""
Expand All @@ -128,27 +131,46 @@ class RangePartition(Partition):
"""
def __init__(self, model, **meta):
super(RangePartition, self).__init__(model, **meta)
self.constraint = meta['constraint']
self.subtype = meta['subtype']
self.constraints = meta['constraints']
self.subtypes = meta['subtypes']

def _get_definitions(self):
"""
Dynamically returns needed definitions depending on the partition subtype.
"""
try:
definitions = getattr(self, '_get_{0}_definitions'.format(self.subtype))()
formatters = dict(constraint=self.constraint, subtype=self.subtype, **definitions.pop('formatters', {}))
return definitions, formatters
except AttributeError:
import re
expression = '_get_(\w+)_function'
raise PartitionRangeSubtypeError(
model=self.model.__name__,
dialect=self.dialect,
current=self.subtype,
allowed=[re.match(expression, c).group(1) for c in dir(self) if re.match(expression, c) is not None])
definitions = dict()
for idx, subtype in enumerate(self.subtypes):
try:
if definitions:
definitions_temp = getattr(self, '_get_{0}_definitions'.format(subtype))(idx)
definitions['formatters'] = {**definitions['formatters'], **definitions_temp['formatters']}
definitions['declarations'] += definitions_temp['declarations']
definitions['variables'] += definitions_temp['variables']
else:
definitions = getattr(self, '_get_{0}_definitions'.format(subtype))(idx)
except AttributeError:
import re
expression = '_get_(\w+)_function'
raise PartitionRangeSubtypeError(
model=self.model.__name__,
dialect=self.dialect,
current=subtype,
allowed=[re.match(expression, c).group(1) for c in dir(self) if
re.match(expression, c) is not None])

def _get_date_definitions(self):
formatters = dict(**definitions.pop('formatters', {}))
tablename = "tablename := '{{parent_table}}'"
checks = "checks := "
for idx in range(len(self.constraints)):
tablename += ' || tablename_{idx}'.format(idx=idx)
formatters["constraint_{}".format(idx)] = self.constraints[idx]
formatters["subtype_{}".format(idx)] = self.subtypes[idx]
checks += " || ' AND ' || ".join(['checks_{idx}'.format(idx=idx) for idx in range(len(self.constraints))])
definitions['variables'].append(tablename+';')
definitions['variables'].append(checks+';')
return definitions, formatters

def _get_date_definitions(self, idx):
"""
Returns definitions for date partition subtype.
"""
Expand All @@ -160,86 +182,124 @@ def _get_date_definitions(self):
}

try:
pattern = patterns[self.constraint]
pattern = patterns[self.constraints[idx]]
except KeyError:
raise PartitionConstraintError(
model=self.model.__name__,
dialect=self.dialect,
current=self.constraint,
current=self.constraints[idx],
allowed=patterns.keys())

return {
'formatters': {'pattern': pattern},
'declarations': [
'match_{idx} {{{{parent_table}}}}.{{{{column_{idx}}}}}%TYPE;'.format(idx=idx),
'tablename_{idx} VARCHAR;'.format(idx=idx),
'checks_{idx} TEXT;'.format(idx=idx),
],
'variables': [
"match := DATE_TRUNC('{constraint}', NEW.{{column}});",
"tablename := '{{parent_table}}_' || TO_CHAR(NEW.{{column}}, '{pattern}');",
"checks := '{{column}} >= ''' || match || ''' AND {{column}} < ''' || (match + INTERVAL '1 {constraint}') || '''';"
"match_{idx} := DATE_TRUNC('{{constraint_{idx}}}', NEW.{{{{column_{idx}}}}});".format(idx=idx),
"tablename_{idx} := '__' || TO_CHAR(NEW.{{{{column_{idx}}}}}, '{{pattern}}');".format(idx=idx),
"checks_{idx} := '{{{{column_{idx}}}}} >= ''' || match_{idx} || ''' AND {{{{column_{idx}}}}} < ''' "
"|| (match_{idx} + INTERVAL '1 {{constraint_{idx}}}') || '''';".format(idx=idx)
]
}

def _get_integer_definitions(self):
def _get_integer_definitions(self, idx):
"""
Returns definitions for integer partition subtype.
"""
if not self.constraint.isdigit() or int(self.constraint) < 1:
if not self.constraints[idx].isdigit() or int(self.constraints[idx]) < 1:
raise PartitionConstraintError(
model=self.model.__name__,
dialect=self.dialect,
current=self.constraint,
current=self.constraints[idx],
allowed=['positive integer'])

return {
'formatters': {'idx': idx},
'declarations': [
'match_{idx} {{{{parent_table}}}}.{{{{column_{idx}}}}}%TYPE;'.format(idx=idx),
'tablename_{idx} VARCHAR;'.format(idx=idx),
'checks_{idx} TEXT;'.format(idx=idx),
],
'variables': [
"IF NEW.{{column}} = 0 THEN",
" tablename := '{{parent_table}}_0';",
" checks := '{{column}} = 0';",
"IF NEW.{{{{column_{idx}}}}} IS NULL THEN".format(idx=idx),
" tablename_{idx} := '__null';".format(idx=idx),
" checks_{idx} := '{{{{column_{idx}}}}} IS NULL';".format(idx=idx),
"ELSE",
" IF NEW.{{column}} > 0 THEN",
" match := ((NEW.{{column}} - 1) / {constraint}) * {constraint} + 1;",
" tablename := '{{parent_table}}_' || match || '_' || (match + {constraint}) - 1;",
" IF NEW.{{{{column_{idx}}}}} = 0 THEN".format(idx=idx),
" tablename_{idx} := '__0';".format(idx=idx),
" checks_{idx} := '{{{{column_{idx}}}}} = 0';".format(idx=idx),
" ELSE",
" match := FLOOR(NEW.{{column}} :: FLOAT / {constraint} :: FLOAT) * {constraint};",
" tablename := '{{parent_table}}_m' || ABS(match) || '_m' || ABS((match + {constraint}) - 1);",
" IF NEW.{{{{column_{idx}}}}} > 0 THEN".format(idx=idx),
" match_{idx} := ((NEW.{{{{column_{idx}}}}} - 1) / "
"{{constraint_{idx}}}) * {{constraint_{idx}}} + 1;".format(idx=idx),
" tablename_{idx} := '__' || match_{idx} || '_' || "
"(match_{idx} + {{constraint_{idx}}}) - 1;".format(idx=idx),
" ELSE",
" match_{idx} := FLOOR(NEW.{{{{column_{idx}}}}} :: FLOAT / "
"{{constraint_{idx}}} :: FLOAT) * {{constraint_{idx}}};".format(idx=idx),
" tablename_{idx} := '__m' || ABS(match_{idx}) || '_m' || "
"ABS((match_{idx} + {{constraint_{idx}}}) - 1);".format(idx=idx),
" END IF;",
" checks_{idx} := '{{{{column_{idx}}}}} >= ' || match_{idx} || "
"' AND {{{{column_{idx}}}}} <= ' || (match_{idx} + {{constraint_{idx}}}) - 1;".format(idx=idx),
" END IF;",
" checks := '{{column}} >= ' || match || ' AND {{column}} <= ' || (match + {constraint}) - 1;",
"END IF;"
"END IF;",

]
}

def _get_string_firstchars_definitions(self):
def _get_string_firstchars_definitions(self, idx):
"""
Returns definitions for string firstchars partition subtype.
"""
if not self.constraint.isdigit() or int(self.constraint) < 1:
if not self.constraints[idx].isdigit() or int(self.constraints[idx]) < 1:
raise PartitionConstraintError(
model=self.model.__name__,
dialect=self.dialect,
current=self.constraint,
current=self.constraints[idx],
allowed=['positive integer'])

return {
'formatters': {},
'declarations': [
'match_{idx} {{{{parent_table}}}}.{{{{column_{idx}}}}}%TYPE;'.format(idx=idx),
'tablename_{idx} VARCHAR;'.format(idx=idx),
'checks_{idx} TEXT;'.format(idx=idx),
],
'variables': [
"match := LOWER(SUBSTR(NEW.{{column}}, 1, {constraint}));",
"tablename := QUOTE_IDENT('{{parent_table}}_' || match);",
"checks := 'LOWER(SUBSTR({{column}}, 1, {constraint})) = ''' || match || '''';"
"match_{idx} := LOWER(SUBSTR(NEW.{{{{column_{idx}}}}}, 1, {{constraint_{idx}}}));".format(idx=idx),
"tablename_{idx} := QUOTE_IDENT('__' || match_{idx});".format(idx=idx),
"checks_{idx} := 'LOWER(SUBSTR({{{{column_{idx}}}}}, 1, "
"{{constraint_{idx}}})) = ''' || match_{idx} || '''';".format(idx=idx)
]
}

def _get_string_lastchars_definitions(self):
def _get_string_lastchars_definitions(self, idx):
"""
Returns definitions for string lastchars partition subtype.
"""
if not self.constraint.isdigit() or int(self.constraint) < 1:
if not self.constraints[idx].isdigit() or int(self.constraints[idx]) < 1:
raise PartitionConstraintError(
model=self.model.__name__,
dialect=self.dialect,
current=self.constraint,
current=self.constraints[idx],
allowed=['positive integer'])

return {
'formatters': {},
'declarations': [
'match_{idx} {{{{parent_table}}}}.{{{{column_{idx}}}}}%TYPE;'.format(idx=idx),
'tablename_{idx} VARCHAR;'.format(idx=idx),
'checks_{idx} TEXT;'.format(idx=idx),
],
'variables': [
"match := LOWER(SUBSTRING(NEW.{{column}} FROM '.{{{{{constraint}}}}}$'));",
"tablename := QUOTE_IDENT('{{parent_table}}_' || match);",
"checks := 'LOWER(SUBSTRING({{column}} FROM ''.{{{{{constraint}}}}}$'')) = ''' || match || '''';"
"match_{idx} := LOWER(SUBSTRING(NEW.{{{{column_{idx}}}}} "
"FROM '.{{{{{{{{{{constraint_{idx}}}}}}}}}}}$'));".format(idx=idx),
"tablename_{idx} := QUOTE_IDENT('__' || match_{idx});".format(idx=idx),
"checks_{idx} := 'LOWER(SUBSTRING({{{{column_{idx}}}}} FROM "
"''.{{{{{{{{{{constraint_{idx}}}}}}}}}}}$'')) = ''' || match_{idx} || '''';".format(idx=idx)
]
}
Loading