592 lines
21 KiB
Python
592 lines
21 KiB
Python
# Copyright 2013-2017 DataStax, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
try:
|
|
import unittest2 as unittest
|
|
except ImportError:
|
|
import unittest # noqa
|
|
|
|
from datetime import datetime, date, time
|
|
from decimal import Decimal
|
|
from mock import Mock
|
|
from uuid import UUID, uuid4
|
|
|
|
from cassandra.cqlengine.models import Model
|
|
from cassandra.cqlengine.usertype import UserType, UserTypeDefinitionException
|
|
from cassandra.cqlengine import columns, connection
|
|
from cassandra.cqlengine.management import sync_table, drop_table, sync_type, create_keyspace_simple, drop_keyspace
|
|
from cassandra.cqlengine import ValidationError
|
|
from cassandra.util import Date, Time
|
|
|
|
from tests.integration import PROTOCOL_VERSION
|
|
from tests.integration.cqlengine.base import BaseCassEngTestCase
|
|
from tests.integration.cqlengine import DEFAULT_KEYSPACE
|
|
|
|
|
|
class User(UserType):
|
|
age = columns.Integer()
|
|
name = columns.Text()
|
|
|
|
|
|
class UserModel(Model):
|
|
id = columns.Integer(primary_key=True)
|
|
info = columns.UserDefinedType(User)
|
|
|
|
|
|
class AllDatatypes(UserType):
|
|
a = columns.Ascii()
|
|
b = columns.BigInt()
|
|
c = columns.Blob()
|
|
d = columns.Boolean()
|
|
e = columns.DateTime()
|
|
f = columns.Decimal()
|
|
g = columns.Double()
|
|
h = columns.Float()
|
|
i = columns.Inet()
|
|
j = columns.Integer()
|
|
k = columns.Text()
|
|
l = columns.TimeUUID()
|
|
m = columns.UUID()
|
|
n = columns.VarInt()
|
|
|
|
|
|
class AllDatatypesModel(Model):
|
|
id = columns.Integer(primary_key=True)
|
|
data = columns.UserDefinedType(AllDatatypes)
|
|
|
|
|
|
class UserDefinedTypeTests(BaseCassEngTestCase):
|
|
|
|
def setUp(self):
|
|
if PROTOCOL_VERSION < 3:
|
|
raise unittest.SkipTest("UDTs require native protocol 3+, currently using: {0}".format(PROTOCOL_VERSION))
|
|
|
|
def test_can_create_udts(self):
|
|
class User(UserType):
|
|
age = columns.Integer()
|
|
name = columns.Text()
|
|
|
|
sync_type(DEFAULT_KEYSPACE, User)
|
|
user = User(age=42, name="John")
|
|
self.assertEqual(42, user.age)
|
|
self.assertEqual("John", user.name)
|
|
|
|
# Add a field
|
|
class User(UserType):
|
|
age = columns.Integer()
|
|
name = columns.Text()
|
|
gender = columns.Text()
|
|
|
|
sync_type(DEFAULT_KEYSPACE, User)
|
|
user = User(age=42)
|
|
user["name"] = "John"
|
|
user["gender"] = "male"
|
|
self.assertEqual(42, user.age)
|
|
self.assertEqual("John", user.name)
|
|
self.assertEqual("male", user.gender)
|
|
|
|
# Remove a field
|
|
class User(UserType):
|
|
age = columns.Integer()
|
|
name = columns.Text()
|
|
|
|
sync_type(DEFAULT_KEYSPACE, User)
|
|
user = User(age=42, name="John", gender="male")
|
|
with self.assertRaises(AttributeError):
|
|
user.gender
|
|
|
|
def test_can_insert_udts(self):
|
|
|
|
sync_table(UserModel)
|
|
self.addCleanup(drop_table, UserModel)
|
|
|
|
user = User(age=42, name="John")
|
|
UserModel.create(id=0, info=user)
|
|
|
|
self.assertEqual(1, UserModel.objects.count())
|
|
|
|
john = UserModel.objects.first()
|
|
self.assertEqual(0, john.id)
|
|
self.assertTrue(type(john.info) is User)
|
|
self.assertEqual(42, john.info.age)
|
|
self.assertEqual("John", john.info.name)
|
|
|
|
def test_can_update_udts(self):
|
|
sync_table(UserModel)
|
|
self.addCleanup(drop_table, UserModel)
|
|
|
|
user = User(age=42, name="John")
|
|
created_user = UserModel.create(id=0, info=user)
|
|
|
|
john_info = UserModel.objects.first().info
|
|
self.assertEqual(42, john_info.age)
|
|
self.assertEqual("John", john_info.name)
|
|
|
|
created_user.info = User(age=22, name="Mary")
|
|
created_user.update()
|
|
|
|
mary_info = UserModel.objects.first().info
|
|
self.assertEqual(22, mary_info["age"])
|
|
self.assertEqual("Mary", mary_info["name"])
|
|
|
|
def test_can_update_udts_with_nones(self):
|
|
sync_table(UserModel)
|
|
self.addCleanup(drop_table, UserModel)
|
|
|
|
user = User(age=42, name="John")
|
|
created_user = UserModel.create(id=0, info=user)
|
|
|
|
john_info = UserModel.objects.first().info
|
|
self.assertEqual(42, john_info.age)
|
|
self.assertEqual("John", john_info.name)
|
|
|
|
created_user.info = None
|
|
created_user.update()
|
|
|
|
john_info = UserModel.objects.first().info
|
|
self.assertIsNone(john_info)
|
|
|
|
def test_can_create_same_udt_different_keyspaces(self):
|
|
sync_type(DEFAULT_KEYSPACE, User)
|
|
|
|
create_keyspace_simple("simplex", 1)
|
|
sync_type("simplex", User)
|
|
drop_keyspace("simplex")
|
|
|
|
def test_can_insert_partial_udts(self):
|
|
class UserGender(UserType):
|
|
age = columns.Integer()
|
|
name = columns.Text()
|
|
gender = columns.Text()
|
|
|
|
class UserModelGender(Model):
|
|
id = columns.Integer(primary_key=True)
|
|
info = columns.UserDefinedType(UserGender)
|
|
|
|
sync_table(UserModelGender)
|
|
self.addCleanup(drop_table, UserModelGender)
|
|
|
|
user = UserGender(age=42, name="John")
|
|
UserModelGender.create(id=0, info=user)
|
|
|
|
john_info = UserModelGender.objects.first().info
|
|
self.assertEqual(42, john_info.age)
|
|
self.assertEqual("John", john_info.name)
|
|
self.assertIsNone(john_info.gender)
|
|
|
|
user = UserGender(age=42)
|
|
UserModelGender.create(id=0, info=user)
|
|
|
|
john_info = UserModelGender.objects.first().info
|
|
self.assertEqual(42, john_info.age)
|
|
self.assertIsNone(john_info.name)
|
|
self.assertIsNone(john_info.gender)
|
|
|
|
def test_can_insert_nested_udts(self):
|
|
class Depth_0(UserType):
|
|
age = columns.Integer()
|
|
name = columns.Text()
|
|
|
|
class Depth_1(UserType):
|
|
value = columns.UserDefinedType(Depth_0)
|
|
|
|
class Depth_2(UserType):
|
|
value = columns.UserDefinedType(Depth_1)
|
|
|
|
class Depth_3(UserType):
|
|
value = columns.UserDefinedType(Depth_2)
|
|
|
|
class DepthModel(Model):
|
|
id = columns.Integer(primary_key=True)
|
|
v_0 = columns.UserDefinedType(Depth_0)
|
|
v_1 = columns.UserDefinedType(Depth_1)
|
|
v_2 = columns.UserDefinedType(Depth_2)
|
|
v_3 = columns.UserDefinedType(Depth_3)
|
|
|
|
sync_table(DepthModel)
|
|
self.addCleanup(drop_table, DepthModel)
|
|
|
|
udts = [Depth_0(age=42, name="John")]
|
|
udts.append(Depth_1(value=udts[0]))
|
|
udts.append(Depth_2(value=udts[1]))
|
|
udts.append(Depth_3(value=udts[2]))
|
|
|
|
DepthModel.create(id=0, v_0=udts[0], v_1=udts[1], v_2=udts[2], v_3=udts[3])
|
|
output = DepthModel.objects.first()
|
|
|
|
self.assertEqual(udts[0], output.v_0)
|
|
self.assertEqual(udts[1], output.v_1)
|
|
self.assertEqual(udts[2], output.v_2)
|
|
self.assertEqual(udts[3], output.v_3)
|
|
|
|
def test_can_insert_udts_with_nones(self):
|
|
"""
|
|
Test for inserting all column types as empty into a UserType as None's
|
|
|
|
test_can_insert_udts_with_nones tests that each cqlengine column type can be inserted into a UserType as None's.
|
|
It first creates a UserType that has each cqlengine column type, and a corresponding table/Model. It then creates
|
|
a UserType instance where all the fields are None's and inserts the UserType as an instance of the Model. Finally,
|
|
it verifies that each column read from the UserType from Cassandra is None.
|
|
|
|
@since 2.5.0
|
|
@jira_ticket PYTHON-251
|
|
@expected_result The UserType is inserted with each column type, and the resulting read yields None's for each column.
|
|
|
|
@test_category data_types:udt
|
|
"""
|
|
sync_table(AllDatatypesModel)
|
|
self.addCleanup(drop_table, AllDatatypesModel)
|
|
|
|
input = AllDatatypes(a=None, b=None, c=None, d=None, e=None, f=None, g=None, h=None, i=None, j=None, k=None,
|
|
l=None, m=None, n=None)
|
|
AllDatatypesModel.create(id=0, data=input)
|
|
|
|
self.assertEqual(1, AllDatatypesModel.objects.count())
|
|
|
|
output = AllDatatypesModel.objects.first().data
|
|
self.assertEqual(input, output)
|
|
|
|
def test_can_insert_udts_with_all_datatypes(self):
|
|
"""
|
|
Test for inserting all column types into a UserType
|
|
|
|
test_can_insert_udts_with_all_datatypes tests that each cqlengine column type can be inserted into a UserType.
|
|
It first creates a UserType that has each cqlengine column type, and a corresponding table/Model. It then creates
|
|
a UserType instance where all the fields have corresponding data, and inserts the UserType as an instance of the Model.
|
|
Finally, it verifies that each column read from the UserType from Cassandra is the same as the input parameters.
|
|
|
|
@since 2.5.0
|
|
@jira_ticket PYTHON-251
|
|
@expected_result The UserType is inserted with each column type, and the resulting read yields proper data for each column.
|
|
|
|
@test_category data_types:udt
|
|
"""
|
|
sync_table(AllDatatypesModel)
|
|
self.addCleanup(drop_table, AllDatatypesModel)
|
|
|
|
input = AllDatatypes(a='ascii', b=2 ** 63 - 1, c=bytearray(b'hello world'), d=True,
|
|
e=datetime.utcfromtimestamp(872835240), f=Decimal('12.3E+7'), g=2.39,
|
|
h=3.4028234663852886e+38, i='123.123.123.123', j=2147483647, k='text',
|
|
l=UUID('FE2B4360-28C6-11E2-81C1-0800200C9A66'),
|
|
m=UUID('067e6162-3b6f-4ae2-a171-2470b63dff00'), n=int(str(2147483647) + '000'))
|
|
AllDatatypesModel.create(id=0, data=input)
|
|
|
|
self.assertEqual(1, AllDatatypesModel.objects.count())
|
|
output = AllDatatypesModel.objects.first().data
|
|
|
|
for i in range(ord('a'), ord('a') + 14):
|
|
self.assertEqual(input[chr(i)], output[chr(i)])
|
|
|
|
def test_can_insert_udts_protocol_v4_datatypes(self):
|
|
"""
|
|
Test for inserting all protocol v4 column types into a UserType
|
|
|
|
test_can_insert_udts_protocol_v4_datatypes tests that each protocol v4 cqlengine column type can be inserted
|
|
into a UserType. It first creates a UserType that has each protocol v4 cqlengine column type, and a corresponding
|
|
table/Model. It then creates a UserType instance where all the fields have corresponding data, and inserts the
|
|
UserType as an instance of the Model. Finally, it verifies that each column read from the UserType from Cassandra
|
|
is the same as the input parameters.
|
|
|
|
@since 2.6.0
|
|
@jira_ticket PYTHON-245
|
|
@expected_result The UserType is inserted with each protocol v4 column type, and the resulting read yields proper data for each column.
|
|
|
|
@test_category data_types:udt
|
|
"""
|
|
|
|
if PROTOCOL_VERSION < 4:
|
|
raise unittest.SkipTest("Protocol v4 datatypes in UDTs require native protocol 4+, currently using: {0}".format(PROTOCOL_VERSION))
|
|
|
|
class Allv4Datatypes(UserType):
|
|
a = columns.Date()
|
|
b = columns.SmallInt()
|
|
c = columns.Time()
|
|
d = columns.TinyInt()
|
|
|
|
class Allv4DatatypesModel(Model):
|
|
id = columns.Integer(primary_key=True)
|
|
data = columns.UserDefinedType(Allv4Datatypes)
|
|
|
|
sync_table(Allv4DatatypesModel)
|
|
self.addCleanup(drop_table, Allv4DatatypesModel)
|
|
|
|
input = Allv4Datatypes(a=Date(date(1970, 1, 1)), b=32523, c=Time(time(16, 47, 25, 7)), d=123)
|
|
Allv4DatatypesModel.create(id=0, data=input)
|
|
|
|
self.assertEqual(1, Allv4DatatypesModel.objects.count())
|
|
output = Allv4DatatypesModel.objects.first().data
|
|
|
|
for i in range(ord('a'), ord('a') + 3):
|
|
self.assertEqual(input[chr(i)], output[chr(i)])
|
|
|
|
def test_nested_udts_inserts(self):
|
|
"""
|
|
Test for inserting collections of user types using cql engine.
|
|
|
|
test_nested_udts_inserts Constructs a model that contains a list of usertypes. It will then attempt to insert
|
|
them. The expectation is that no exception is thrown during insert. For sanity sake we also validate that our
|
|
input and output values match. This combination of model, and UT produces a syntax error in 2.5.1 due to
|
|
improper quoting around the names collection.
|
|
|
|
@since 2.6.0
|
|
@jira_ticket PYTHON-311
|
|
@expected_result No syntax exception thrown
|
|
|
|
@test_category data_types:udt
|
|
"""
|
|
|
|
class Name(UserType):
|
|
type_name__ = "header"
|
|
|
|
name = columns.Text()
|
|
value = columns.Text()
|
|
|
|
class Container(Model):
|
|
id = columns.UUID(primary_key=True, default=uuid4)
|
|
names = columns.List(columns.UserDefinedType(Name))
|
|
|
|
# Construct the objects and insert them
|
|
names = []
|
|
for i in range(0, 10):
|
|
names.append(Name(name="name{0}".format(i), value="value{0}".format(i)))
|
|
|
|
# Create table, insert data
|
|
sync_table(Container)
|
|
self.addCleanup(drop_table, Container)
|
|
|
|
Container.create(id=UUID('FE2B4360-28C6-11E2-81C1-0800200C9A66'), names=names)
|
|
|
|
# Validate input and output matches
|
|
self.assertEqual(1, Container.objects.count())
|
|
names_output = Container.objects.first().names
|
|
self.assertEqual(names_output, names)
|
|
|
|
def test_udts_with_unicode(self):
|
|
"""
|
|
Test for inserting models with unicode and udt columns.
|
|
|
|
test_udts_with_unicode constructs a model with a user defined type. It then attempts to insert that model with
|
|
a unicode primary key. It will also attempt to upsert a udt that contains unicode text.
|
|
|
|
@since 3.0.0
|
|
@jira_ticket PYTHON-353
|
|
@expected_result No exceptions thrown
|
|
|
|
@test_category data_types:udt
|
|
"""
|
|
ascii_name = 'normal name'
|
|
unicode_name = u'Fran\u00E7ois'
|
|
|
|
class UserModelText(Model):
|
|
id = columns.Text(primary_key=True)
|
|
info = columns.UserDefinedType(User)
|
|
|
|
sync_table(UserModelText)
|
|
self.addCleanup(drop_table, UserModelText)
|
|
|
|
# Two udt instances one with a unicode one with ascii
|
|
user_template_ascii = User(age=25, name=ascii_name)
|
|
user_template_unicode = User(age=25, name=unicode_name)
|
|
|
|
UserModelText.create(id=ascii_name, info=user_template_unicode)
|
|
UserModelText.create(id=unicode_name, info=user_template_ascii)
|
|
UserModelText.create(id=unicode_name, info=user_template_unicode)
|
|
|
|
def test_register_default_keyspace(self):
|
|
|
|
from cassandra.cqlengine import models
|
|
from cassandra.cqlengine import connection
|
|
|
|
# None emulating no model and no default keyspace before connecting
|
|
connection.udt_by_keyspace.clear()
|
|
User.register_for_keyspace(None)
|
|
self.assertEqual(len(connection.udt_by_keyspace), 1)
|
|
self.assertIn(None, connection.udt_by_keyspace)
|
|
|
|
# register should be with default keyspace, not None
|
|
cluster = Mock()
|
|
connection._register_known_types(cluster)
|
|
cluster.register_user_type.assert_called_with(models.DEFAULT_KEYSPACE, User.type_name(), User)
|
|
|
|
def test_db_field_override(self):
|
|
"""
|
|
Tests for db_field override
|
|
|
|
Tests to ensure that udt's in models can specify db_field for a particular field and that it will be honored.
|
|
|
|
@since 3.1.0
|
|
@jira_ticket PYTHON-346
|
|
@expected_result The actual cassandra column will use the db_field specified.
|
|
|
|
@test_category data_types:udt
|
|
"""
|
|
class db_field_different(UserType):
|
|
age = columns.Integer(db_field='a')
|
|
name = columns.Text(db_field='n')
|
|
|
|
class TheModel(Model):
|
|
id = columns.Integer(primary_key=True)
|
|
info = columns.UserDefinedType(db_field_different)
|
|
|
|
sync_table(TheModel)
|
|
self.addCleanup(drop_table, TheModel)
|
|
|
|
cluster = connection.get_cluster()
|
|
type_meta = cluster.metadata.keyspaces[TheModel._get_keyspace()].user_types[db_field_different.type_name()]
|
|
|
|
type_fields = (db_field_different.age.column, db_field_different.name.column)
|
|
|
|
self.assertEqual(len(type_meta.field_names), len(type_fields))
|
|
for f in type_fields:
|
|
self.assertIn(f.db_field_name, type_meta.field_names)
|
|
|
|
id = 0
|
|
age = 42
|
|
name = 'John'
|
|
info = db_field_different(age=age, name=name)
|
|
TheModel.create(id=id, info=info)
|
|
|
|
self.assertEqual(1, TheModel.objects.count())
|
|
|
|
john = TheModel.objects.first()
|
|
self.assertEqual(john.id, id)
|
|
info = john.info
|
|
self.assertIsInstance(info, db_field_different)
|
|
self.assertEqual(info.age, age)
|
|
self.assertEqual(info.name, name)
|
|
# also excercise the db_Field mapping
|
|
self.assertEqual(info.a, age)
|
|
self.assertEqual(info.n, name)
|
|
|
|
def test_db_field_overload(self):
|
|
"""
|
|
Tests for db_field UserTypeDefinitionException
|
|
|
|
Test so that when we override a model's default field witha db_field that it errors appropriately
|
|
|
|
@since 3.1.0
|
|
@jira_ticket PYTHON-346
|
|
@expected_result Setting a db_field to an existing field causes an exception to occur.
|
|
|
|
@test_category data_types:udt
|
|
"""
|
|
|
|
with self.assertRaises(UserTypeDefinitionException):
|
|
class something_silly(UserType):
|
|
first_col = columns.Integer()
|
|
second_col = columns.Text(db_field='first_col')
|
|
|
|
with self.assertRaises(UserTypeDefinitionException):
|
|
class something_silly_2(UserType):
|
|
first_col = columns.Integer(db_field="second_col")
|
|
second_col = columns.Text()
|
|
|
|
def test_set_udt_fields(self):
|
|
# PYTHON-502
|
|
|
|
u = User()
|
|
u.age = 20
|
|
self.assertEqual(20, u.age)
|
|
|
|
def test_default_values(self):
|
|
"""
|
|
Test that default types are set on object creation for UDTs
|
|
|
|
@since 3.7.0
|
|
@jira_ticket PYTHON-606
|
|
@expected_result Default values should be set.
|
|
|
|
@test_category data_types:udt
|
|
"""
|
|
|
|
class NestedUdt(UserType):
|
|
|
|
test_id = columns.UUID(default=uuid4)
|
|
something = columns.Text()
|
|
default_text = columns.Text(default="default text")
|
|
|
|
class OuterModel(Model):
|
|
|
|
name = columns.Text(primary_key=True)
|
|
first_name = columns.Text()
|
|
nested = columns.List(columns.UserDefinedType(NestedUdt))
|
|
simple = columns.UserDefinedType(NestedUdt)
|
|
|
|
sync_table(OuterModel)
|
|
self.addCleanup(drop_table, OuterModel)
|
|
|
|
t = OuterModel.create(name='test1')
|
|
t.nested = [NestedUdt(something='test')]
|
|
t.simple = NestedUdt(something="")
|
|
t.save()
|
|
self.assertIsNotNone(t.nested[0].test_id)
|
|
self.assertEqual(t.nested[0].default_text, "default text")
|
|
self.assertIsNotNone(t.simple.test_id)
|
|
self.assertEqual(t.simple.default_text, "default text")
|
|
|
|
def test_udt_validate(self):
|
|
"""
|
|
Test to verify restrictions are honored and that validate is called
|
|
for each member of the UDT when an updated is attempted
|
|
|
|
@since 3.10
|
|
@jira_ticket PYTHON-505
|
|
@expected_result a validation error is arisen due to the name being
|
|
too long
|
|
|
|
@test_category data_types:object_mapper
|
|
"""
|
|
class UserValidate(UserType):
|
|
age = columns.Integer()
|
|
name = columns.Text(max_length=2)
|
|
|
|
class UserModelValidate(Model):
|
|
id = columns.Integer(primary_key=True)
|
|
info = columns.UserDefinedType(UserValidate)
|
|
|
|
sync_table(UserModelValidate)
|
|
self.addCleanup(drop_table, UserModelValidate)
|
|
|
|
user = UserValidate(age=1, name="Robert")
|
|
item = UserModelValidate(id=1, info=user)
|
|
with self.assertRaises(ValidationError):
|
|
item.save()
|
|
|
|
def test_udt_validate_with_default(self):
|
|
"""
|
|
Test to verify restrictions are honored and that validate is called
|
|
on the default value
|
|
|
|
@since 3.10
|
|
@jira_ticket PYTHON-505
|
|
@expected_result a validation error is arisen due to the name being
|
|
too long
|
|
|
|
@test_category data_types:object_mapper
|
|
"""
|
|
class UserValidateDefault(UserType):
|
|
age = columns.Integer()
|
|
name = columns.Text(max_length=2, default="Robert")
|
|
|
|
class UserModelValidateDefault(Model):
|
|
id = columns.Integer(primary_key=True)
|
|
info = columns.UserDefinedType(UserValidateDefault)
|
|
|
|
sync_table(UserModelValidateDefault)
|
|
self.addCleanup(drop_table, UserModelValidateDefault)
|
|
|
|
user = UserValidateDefault(age=1)
|
|
item = UserModelValidateDefault(id=1, info=user)
|
|
with self.assertRaises(ValidationError):
|
|
item.save()
|