.. changelog::
:version: 1.1.0b2
+ .. change::
+ :tags: bug, sql
+ :tickets: 3725
+
+ Rolled back the validation rules a bit in :class:`.Enum` to allow
+ unknown string values to pass through, unless the flag
+ ``validate_string=True`` is passed to the Enum; any other kind of object is
+ still of course rejected. While the immediate use
+ is to allow comparisons to enums with LIKE, the fact that this
+ use exists indicates there may be more unknown-string-comparsion use
+ cases than we expected, which hints that perhaps there are some
+ unknown string-INSERT cases too.
+
.. changelog::
:version: 1.1.0b1
:released: June 16, 2016
To accomodate for Python native enumerated objects, as well as for edge
cases such as that of where a non-native ENUM type is used within an ARRAY
and a CHECK contraint is infeasible, the :class:`.Enum` datatype now adds
-in-Python validation of input values::
+in-Python validation of input values when the :paramref:`.Enum.validate_strings`
+flag is used (1.1.0b2)::
>>> from sqlalchemy import Table, MetaData, Column, Enum, create_engine
>>> t = Table(
... 'data', MetaData(),
- ... Column('value', Enum("one", "two", "three"))
+ ... Column('value', Enum("one", "two", "three", validate_strings=True))
... )
>>> e = create_engine("sqlite://")
>>> t.create(e)
[SQL: u'INSERT INTO data (value) VALUES (?)']
[parameters: [{'value': 'four'}]]
-For simplicity and consistency, this validation is now turned on in all cases,
-whether or not the enumerated type uses a database-native form, whether
-or not the CHECK constraint is in use, as well as whether or not a
-PEP-435 enumerated type or plain list of string values is used. The
-check also occurs on the result-handling side as well, when values coming
-from the database are returned.
+This validation is turned off by default as there are already use cases
+identified where users don't want such validation (such as string comparisons).
+For non-string types, it necessarily takes place in all cases. The
+check also occurs unconditionally on the result-handling side as well, when
+values coming from the database are returned.
This validation is in addition to the existing behavior of creating a
CHECK constraint when a non-native enumerated type is used. The creation of
"""
kw.pop('strict', None)
- sqltypes.Enum.__init__(self, *enums)
+ validate_strings = kw.pop("validate_strings", False)
+ sqltypes.Enum.__init__(
+ self, validate_strings=validate_strings, *enums)
kw.pop('metadata', None)
kw.pop('schema', None)
kw.pop('name', None)
from .. import inspection
from .. import event
from ..util import pickle
+from ..util import compat
import decimal
if util.jython:
``schema`` attribute. This also takes effect when using the
:meth:`.Table.tometadata` operation.
+ :param validate_strings: when True, invalid string values will
+ be validated and not be allowed to pass through.
+
+ .. versionadded:: 1.1.0b2
+
"""
values, objects = self._parse_into_values(enums, kw)
self.native_enum = kw.pop('native_enum', True)
convert_unicode = kw.pop('convert_unicode', None)
self.create_constraint = kw.pop('create_constraint', True)
+ self.validate_strings = kw.pop('validate_strings', False)
+
if convert_unicode is None:
for e in self.enums:
if isinstance(e, util.text_type):
try:
return self._valid_lookup[elem]
except KeyError:
- raise LookupError(
- '"%s" is not among the defined enum values' % elem)
+ # for unknown string values, we return as is. While we can
+ # validate these if we wanted, that does not allow for lesser-used
+ # end-user use cases, such as using a LIKE comparison with an enum,
+ # or for an application that wishes to apply string tests to an
+ # ENUM (see [ticket:3725]). While we can decide to differentiate
+ # here between an INSERT statement and a criteria used in a SELECT,
+ # for now we're staying conservative w/ behavioral changes (perhaps
+ # someone has a trigger that handles strings on INSERT)
+ if not self.validate_strings and \
+ isinstance(elem, compat.string_types):
+ return elem
+ else:
+ raise LookupError(
+ '"%s" is not among the defined enum values' % elem)
def _object_value_for_elem(self, elem):
try:
convert_unicode=self.convert_unicode,
native_enum=self.native_enum,
inherit_schema=self.inherit_schema,
+ validate_strings=self.validate_strings,
_create_events=_create_events,
*args,
**kw)
'mysql_enum', self.metadata,
Column('e1', e1),
Column('e2', e2, nullable=False),
- Column('e2generic', Enum("a", "b"), nullable=False),
+ Column(
+ 'e2generic',
+ Enum("a", "b", validate_strings=True), nullable=False),
Column('e3', e3),
Column('e4', e4,
nullable=False),
Column('someenum', Enum('one', 'two', 'three', native_enum=False)),
Column('someotherenum',
Enum('one', 'two', 'three',
- create_constraint=False, native_enum=False)),
+ create_constraint=False, native_enum=False,
+ validate_strings=True)),
)
Table(
def test_validators_pep435(self):
type_ = Enum(self.SomeEnum)
+ validate_type = Enum(self.SomeEnum, validate_strings=True)
bind_processor = type_.bind_processor(testing.db.dialect)
+ bind_processor_validates = validate_type.bind_processor(
+ testing.db.dialect)
eq_(bind_processor('one'), "one")
eq_(bind_processor(self.one), "one")
+ eq_(bind_processor("foo"), "foo")
+ assert_raises_message(
+ LookupError,
+ '"5" is not among the defined enum values',
+ bind_processor, 5
+ )
+
assert_raises_message(
LookupError,
'"foo" is not among the defined enum values',
- bind_processor, "foo"
+ bind_processor_validates, "foo"
)
result_processor = type_.result_processor(testing.db.dialect, None)
)
literal_processor = type_.literal_processor(testing.db.dialect)
+ validate_literal_processor = validate_type.literal_processor(
+ testing.db.dialect)
eq_(literal_processor("one"), "'one'")
+
+ eq_(literal_processor("foo"), "'foo'")
+
+ assert_raises_message(
+ LookupError,
+ '"5" is not among the defined enum values',
+ literal_processor, 5
+ )
+
assert_raises_message(
LookupError,
'"foo" is not among the defined enum values',
- literal_processor, "foo"
+ validate_literal_processor, "foo"
)
def test_validators_plain(self):
type_ = Enum("one", "two")
+ validate_type = Enum("one", "two", validate_strings=True)
bind_processor = type_.bind_processor(testing.db.dialect)
+ bind_processor_validates = validate_type.bind_processor(
+ testing.db.dialect)
eq_(bind_processor('one'), "one")
+ eq_(bind_processor('foo'), "foo")
+ assert_raises_message(
+ LookupError,
+ '"5" is not among the defined enum values',
+ bind_processor, 5
+ )
+
assert_raises_message(
LookupError,
'"foo" is not among the defined enum values',
- bind_processor, "foo"
+ bind_processor_validates, "foo"
)
result_processor = type_.result_processor(testing.db.dialect, None)
)
literal_processor = type_.literal_processor(testing.db.dialect)
+ validate_literal_processor = validate_type.literal_processor(
+ testing.db.dialect)
eq_(literal_processor("one"), "'one'")
+ eq_(literal_processor("foo"), "'foo'")
+ assert_raises_message(
+ LookupError,
+ '"5" is not among the defined enum values',
+ literal_processor, 5
+ )
+
assert_raises_message(
LookupError,
'"foo" is not among the defined enum values',
- literal_processor, "foo"
+ validate_literal_processor, "foo"
)
+ def test_validators_not_in_like_roundtrip(self):
+ enum_table = self.tables['non_native_enum_table']
+
+ enum_table.insert().execute([
+ {'id': 1, 'someenum': 'two'},
+ {'id': 2, 'someenum': 'two'},
+ {'id': 3, 'someenum': 'one'},
+ ])
+
+ eq_(
+ enum_table.select().
+ where(enum_table.c.someenum.like('%wo%')).
+ order_by(enum_table.c.id).execute().fetchall(),
+ [
+ (1, 'two', None),
+ (2, 'two', None),
+ ]
+ )
@testing.fails_on(
'postgresql+zxjdbc',
assert_raises(
exc.StatementError,
self.tables['non_native_enum_table'].insert().execute,
- {'id': 4, 'someenum': 'four'}
+ {'id': 4, 'someotherenum': 'four'}
)
def test_mock_engine_no_prob(self):