Skip to content

Commit

Permalink
Implemented constraints loading to a database (#83)
Browse files Browse the repository at this point in the history
* Implemented min/maxLength and minimum/maximum

* Implemented pattern

* Implemented enum

* Fixed tests

* Improved how we name enum types
  • Loading branch information
roll committed Oct 25, 2019
1 parent 1a89e81 commit 11b41b2
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 6 deletions.
27 changes: 21 additions & 6 deletions tableschema_sql/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import json

import six
import sqlalchemy as sa
import tableschema
import sqlalchemy as sa
from sqlalchemy import CheckConstraint as Check
from sqlalchemy.dialects.postgresql import ARRAY, JSON, JSONB, UUID


Expand Down Expand Up @@ -40,7 +41,7 @@ def convert_descriptor(self, bucket, descriptor, index_fields=[], autoincrement=
constraints = []
column_mapping = {}
table_name = self.convert_bucket(bucket)
table_comment = _get_comment(descriptor.get('title', ''), descriptor.get('description', ''))
comment = _get_comment(descriptor.get('title', ''), descriptor.get('description', ''))
schema = tableschema.Schema(descriptor)

# Autoincrement
Expand All @@ -55,10 +56,24 @@ def convert_descriptor(self, bucket, descriptor, index_fields=[], autoincrement=
column_type = sa.Text
fallbacks.append(field.name)
nullable = not field.required
table_comment = _get_field_comment(field)
comment = _get_field_comment(field)
unique = field.constraints.get('unique', False)
column = sa.Column(field.name, column_type, nullable=nullable, comment=table_comment,
unique=unique)
checks = []
for name, value in field.constraints.items():
if name == 'minLength':
checks.append(Check('LENGTH("%s") >= %s' % (field.name, value)))
elif name == 'maxLength':
checks.append(Check('LENGTH("%s") <= %s' % (field.name, value)))
elif name == 'minimum':
checks.append(Check('"%s" >= %s' % (field.name, value)))
elif name == 'maximum':
checks.append(Check('"%s" <= %s' % (field.name, value)))
elif name == 'pattern':
checks.append(Check('"%s" like \'%s\'' % (field.name, value)))
elif name == 'enum':
column_type = sa.Enum(*value, name='%s_%s_enum' % (table_name, field.name))
column = sa.Column(*([field.name, column_type] + checks),
nullable=nullable, comment=comment, unique=unique)
columns.append(column)
column_mapping[field.name] = column

Expand Down Expand Up @@ -101,7 +116,7 @@ def convert_descriptor(self, bucket, descriptor, index_fields=[], autoincrement=
index_columns = [column_mapping[field] for field in index_definition]
indexes.append(sa.Index(name, *index_columns))

return columns, constraints, indexes, fallbacks, table_comment
return columns, constraints, indexes, fallbacks, comment

def convert_row(self, keyed_row, schema, fallbacks):
"""Convert row to SQL
Expand Down
58 changes: 58 additions & 0 deletions tests/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import json
import pytest
import tableschema
import sqlalchemy as sa
from copy import deepcopy
from tabulator import Stream
from sqlalchemy import create_engine
Expand Down Expand Up @@ -567,6 +568,63 @@ def test_storage_autoincrement_mapping(dialect, database_url):
]


@pytest.mark.parametrize('dialect, database_url', [
('postgresql', os.environ['POSTGRES_URL']),
('sqlite', os.environ['SQLITE_URL']),
# ('mysql', os.environ['MYSQL_URL']),
])
def test_storage_constraints(dialect, database_url):
schema = {
'fields': [
{'name': 'stringMinLength', 'type': 'string', 'constraints': {'minLength': 5}},
{'name': 'stringMaxLength', 'type': 'string', 'constraints': {'maxLength': 5}},
{'name': 'numberMinimum', 'type': 'number', 'constraints': {'minimum': 5}},
{'name': 'numberMaximum', 'type': 'number', 'constraints': {'maximum': 5}},
{'name': 'stringPattern', 'type': 'string', 'constraints': {'pattern': 'test'}},
{'name': 'stringEnum', 'type': 'string', 'constraints': {'enum': ['test']}},
]
}

# Create table
engine = create_engine(database_url)
storage = Storage(engine, prefix='test_storage_constraints_')
storage.create('bucket', schema, force=True)
table_name = 'test_storage_constraints_bucket'

# Write valid data
storage.write('bucket', [['aaaaa', 'aaaaa', 5, 5, 'test', 'test']])

# Write invalid data (stringMinLength)
with pytest.raises(sa.exc.IntegrityError) as excinfo:
pattern = "INSERT INTO %s VALUES('a', 'aaaaa', 5, 5, 'test', 'test')"
engine.execute(pattern % table_name)

# Write invalid data (stringMaxLength)
with pytest.raises(sa.exc.IntegrityError) as excinfo:
pattern = "INSERT INTO %s VALUES('aaaaa', 'aaaaaaaaa', 5, 5, 'test', 'test')"
engine.execute(pattern % table_name)

# Write invalid data (numberMinimum)
with pytest.raises(sa.exc.IntegrityError) as excinfo:
pattern = "INSERT INTO %s VALUES('aaaaa', 'aaaaa', 1, 5, 'test', 'test')"
engine.execute(pattern % table_name)

# Write invalid data (numberMaximum)
with pytest.raises(sa.exc.IntegrityError) as excinfo:
pattern = "INSERT INTO %s VALUES('aaaaa', 'aaaaa', 5, 9, 'test', 'test')"
engine.execute(pattern % table_name)

# Write invalid data (stringPattern)
with pytest.raises(sa.exc.IntegrityError) as excinfo:
pattern = "INSERT INTO %s VALUES('aaaaa', 'aaaaa', 5, 5, 'bad', 'test')"
engine.execute(pattern % table_name)

# Write invalid data (stringEnum)
with pytest.raises((sa.exc.DataError, sa.exc.IntegrityError)) as excinfo:
pattern = "INSERT INTO %s VALUES('aaaaa', 'aaaaa', 5, 5, 'test', 'bad')"
engine.execute(pattern % table_name)


# Helpers

def cast(resource, skip=[]):
Expand Down

0 comments on commit 11b41b2

Please sign in to comment.