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

#73 Convert CID to SQL statements #96

Closed
wants to merge 12 commits into from
8 changes: 2 additions & 6 deletions cutplace/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,8 @@ def __init__(self, field_name, is_allowed_to_be_empty, length_text, rule, data_f
self.valid_range = ranges.DecimalRange(rule, ranges.DEFAULT_DECIMAL_RANGE_TEXT)
self._length = ranges.DecimalRange(length_text)

if self.valid_range is not None:
self._precision = self.valid_range.precision
self._scale = self.valid_range.scale
else:
self._precision = None
self._scale = None
self._precision = self.valid_range.precision
self._scale = self.valid_range.scale

def validated_value(self, value):
assert value
Expand Down
5 changes: 3 additions & 2 deletions cutplace/ranges.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ def __init__(self, description, default=None):
raise errors.InterfaceError(
'value for %s must a number, a single character or a symbolic name but is: %s'
% (name_for_code, _compat.text_repr(next_value)), location)

if ellipsis_found:
if upper is None:
upper = value_as_int
Expand Down Expand Up @@ -281,7 +282,7 @@ def __init__(self, description, default=None):
# Handle "...".
raise errors.InterfaceError(
'ellipsis (...) must be preceded and/or succeeded by number', location)
else:
else: # maybe useless?!
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace whole else clause with assert False.

# Handle "".
result = None
else:
Expand Down Expand Up @@ -427,7 +428,7 @@ def _item_contains(self, item, value):
lower = item[0]
upper = item[1]
if lower is None:
if upper is None:
if upper is None: # ??
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned this up with #97, just merge.

# Handle ""
result = True
else:
Expand Down
27 changes: 26 additions & 1 deletion cutplace/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from __future__ import unicode_literals

import os.path
import six
import sqlite3

from cutplace import _tools
Expand Down Expand Up @@ -177,7 +178,7 @@ def as_sql_create_table(cid, dialect='ansi'):
constraints = ""

# get column definitions and constraints for all fields
for field in cid._field_formats:
for field in cid.field_formats:
column_def, constraint = field.as_sql(dialect)
result += column_def + ",\n"

Expand Down Expand Up @@ -207,3 +208,27 @@ def as_sql_create_table(cid, dialect='ansi'):
cursor.close()

return result


def as_sql_create_inserts(cid, source_data_reader):
"""
:param Cid cid:
:param validio.Reader source_data_reader:
:return:
"""
assert cid
assert source_data_reader

file_name = os.path.basename(cid._cid_path)
table_name = file_name.split('.')[0]

for row in source_data_reader.rows():
for i in range(len(row)):

# HACK: can't use isinstance() function because of circular dependency when importing fields module
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still have a circular dependency?

fiel_type = six.text_type((cid.field_formats[i]).__class__.__name__)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: fiel_type --> field_type.

if fiel_type not in ('IntegerFieldFormat', 'DecimalFieldFormat'):
row[i] = "'" + row[i] + "'"

result = "insert into %s(%s) values (%s);" % (table_name, ', '.join(cid.field_names), ', '.join(row))
yield result
16 changes: 16 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ def test_fails_on_invalid_character(self):
"character 'x' (code point U+0078, decimal 120) in field 'something' at column 3 must be an allowed"
" character: 97...99", six.text_type(anticipated_error))

def test_can_raise_not_implemented_error(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to test for NotImplementedError, please remove this test case.

field_format = fields.AbstractFieldFormat('x', False, '3...5', '', _ANY_FORMAT)
self.assertRaises(NotImplementedError, field_format.validated_value, 4)

def test_can_output_field_format_as_string(self):
field_format = fields.AbstractFieldFormat('x', False, '3...5', '', _ANY_FORMAT)
self.assertEqual(six.text_type(field_format), "AbstractFieldFormat('x', False, Range('3...5'), '')")


class DateTimeFieldFormatTest(unittest.TestCase):
"""
Expand Down Expand Up @@ -475,6 +483,14 @@ def test_fails_on_value_not_matching_pattern(self):
self.assertRaises(errors.FieldValueError, field_format.validated, "hang")


class PublicFieldFunctionTest(unittest.TestCase):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migrate this test to the test_interface and make it a variant of test_fails_on_broken_field_name. As discussed the error message field name must begin with a lower-case letter but is ... should actually be ...must be an ASCII letter....

"""
Test for public functions in the fields module
"""
def test_fails_on_non_ascii_character(self):
self.assertRaises(errors.InterfaceError, fields.validated_field_name, "a�")


if __name__ == '__main__': # pragma: no cover
logging.basicConfig()
logging.getLogger("cutplace").setLevel(logging.INFO)
Expand Down
32 changes: 32 additions & 0 deletions tests/test_ranges.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ def test_fails_on_string_with_multiple_characters(self):
def test_fails_on_unterminated_string(self):
self.assertRaises(errors.InterfaceError, ranges.Range, "\"\"")

def test_fails_on_missing_numbers(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to test_fails_on_ellipsis_without_number.

self.assertRaises(errors.InterfaceError, ranges.Range, "...")

def test_can_validate_with_lower_and_upper_limit(self):
lower_and_upper_range = ranges.Range("-1...1")
lower_and_upper_range.validate("x", - 1)
Expand Down Expand Up @@ -202,12 +205,25 @@ def test_can_create_range_from_length(self):
ranges.create_range_from_length(
ranges.Range("3...4, 10...")).items, [(-999, -10), (100, 9999), (None, -100000000), (1000000000, None)])

def test_can_display_lower_is_upper_length(self):
self.assertEqual(six.text_type(ranges.Range("9...9")), "9")

def test_can_display_multi_range(self):
self.assertEqual(six.text_type(ranges.Range("9...10, 11...13")), "9...10, 11...13")

def test_fails_on_create_range_from_negative_length(self):
self.assertRaises(errors.RangeValueError, ranges.create_range_from_length, ranges.Range("-19...-1"))
self.assertRaises(errors.RangeValueError, ranges.create_range_from_length, ranges.Range("-19...1"))
self.assertRaises(errors.RangeValueError, ranges.create_range_from_length, ranges.Range("-1...0"))
self.assertRaises(errors.RangeValueError, ranges.create_range_from_length, ranges.Range("0...0"))

def test_can_represent_range_containing_none(self):
none_range = ranges.Range(None)
self.assertEqual(six.text_type(none_range), six.text_type(None))
upper_none_range = ranges.Range("1...")
upper_none_range.items.append(None)
self.assertEqual(six.text_type(upper_none_range), "1..., None")


class DecimalRangeTest(unittest.TestCase):

Expand Down Expand Up @@ -408,6 +424,22 @@ def test_can_handle_scientific_notation(self):
self.assertEqual(7, decimal_range.scale)
self.assertEqual(2, decimal_range.precision)

def test_can_display_lower_is_upper_length(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to test_can_represent_equal_lower_and_upper_length.

self.assertEqual(six.text_type(ranges.DecimalRange("9.9...9.9")), "9.9...9.9")

def test_can_display_multi_range(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to test_can_represent_multi_range.

self.assertEqual(six.text_type(ranges.DecimalRange("9.1...10.9, 11...13")), "9.1...10.9, 11.0...13.0")

def test_can_represent_range_containing_none(self):
none_range = ranges.DecimalRange(None)
self.assertEqual(six.text_type(none_range), six.text_type(None))
upper_none_range = ranges.DecimalRange("1...")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which parts of the code is this test supposed to cover that are not already covered by the other tests? Adding items after the constructor should not be supported by Range and DecimalRange. Maybe we should make items a property that returns a tuple (that cannot be modified) instead of a list.

upper_none_range.items.append(None)
self.assertEqual(six.text_type(upper_none_range), "1, None")

def test_fails_on_non_decimal_value_to_validate(self):
decimal_range = ranges.DecimalRange("1.1...2.2")
self.assertRaises(errors.RangeValueError, decimal_range.validate, "x", "a")

if __name__ == "__main__": # pragma: no cover
unittest.main()
13 changes: 13 additions & 0 deletions tests/test_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from cutplace import fields
from cutplace import interface
from cutplace import sql
from cutplace import validio
from tests import dev_test

import unittest

Expand Down Expand Up @@ -116,3 +118,14 @@ def test_can_create_sql_statement(self):
"\nconstraint chk_customer_id check( ( customer_id between 0 and 99999 ) ),"
"\nconstraint chk_length_surname check (length(surname >= 1) and length(surname <= 60)),"
"\nconstraint chk_rule_gender check( gender in ('male','female','unknown') )\n);")

# check if create insert statements works
with validio.Reader(cid_reader, dev_test.path_to_test_data("valid_customers.csv")) as reader:
self.assertEqual(
list(sql.as_sql_create_inserts(cid_reader, reader)),
["insert into customers(branch_id, customer_id, first_name, surname, gender, date_of_birth) "
"values ('38000', 23, 'John', 'Doe', 'male', '08.03.1957');",
"insert into customers(branch_id, customer_id, first_name, surname, gender, date_of_birth) "
"values ('38000', 59, 'Jane', 'Miller', 'female', '04.10.1946');",
"insert into customers(branch_id, customer_id, first_name, surname, gender, date_of_birth) "
"values ('38053', 17, 'Mike', 'Webster', 'male', '23.12.1974');"])