# Copyright 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.
from __future__ import absolute_import

try:
    import unittest2 as unittest
except ImportError:
    import unittest  # noqa

from datetime import datetime
from uuid import uuid4
from packaging.version import Version
import uuid

from cassandra.cluster import Session
from cassandra import InvalidRequest
from tests.integration.cqlengine.base import BaseCassEngTestCase
from cassandra.cqlengine.connection import NOT_SET
import mock
from cassandra.cqlengine import functions
from cassandra.cqlengine.management import sync_table, drop_table
from cassandra.cqlengine.models import Model
from cassandra.cqlengine import columns
from cassandra.cqlengine import query
from cassandra.cqlengine.query import QueryException, BatchQuery
from datetime import timedelta
from datetime import tzinfo

from cassandra.cqlengine import statements
from cassandra.cqlengine import operators
from cassandra.util import uuid_from_time
from cassandra.cqlengine.connection import get_session
from tests.integration import PROTOCOL_VERSION, CASSANDRA_VERSION, greaterthancass20, greaterthancass21, \
    greaterthanorequalcass30
from tests.integration.cqlengine import execute_count


class TzOffset(tzinfo):
    """Minimal implementation of a timezone offset to help testing with timezone
    aware datetimes.
    """

    def __init__(self, offset):
        self._offset = timedelta(hours=offset)

    def utcoffset(self, dt):
        return self._offset

    def tzname(self, dt):
        return 'TzOffset: {}'.format(self._offset.hours)

    def dst(self, dt):
        return timedelta(0)


class TestModel(Model):

    test_id = columns.Integer(primary_key=True)
    attempt_id = columns.Integer(primary_key=True)
    description = columns.Text()
    expected_result = columns.Integer()
    test_result = columns.Integer()


class IndexedTestModel(Model):

    test_id = columns.Integer(primary_key=True)
    attempt_id = columns.Integer(index=True)
    description = columns.Text()
    expected_result = columns.Integer()
    test_result = columns.Integer(index=True)


class IndexedCollectionsTestModel(Model):

    test_id = columns.Integer(primary_key=True)
    attempt_id = columns.Integer(index=True)
    description = columns.Text()
    expected_result = columns.Integer()
    test_result = columns.Integer(index=True)
    test_list = columns.List(columns.Integer, index=True)
    test_set = columns.Set(columns.Integer, index=True)
    test_map = columns.Map(columns.Text, columns.Integer, index=True)

    test_list_no_index = columns.List(columns.Integer, index=False)
    test_set_no_index = columns.Set(columns.Integer, index=False)
    test_map_no_index = columns.Map(columns.Text, columns.Integer, index=False)


class TestMultiClusteringModel(Model):

    one = columns.Integer(primary_key=True)
    two = columns.Integer(primary_key=True)
    three = columns.Integer(primary_key=True)


class TestQuerySetOperation(BaseCassEngTestCase):

    def test_query_filter_parsing(self):
        """
        Tests the queryset filter method parses it's kwargs properly
        """
        query1 = TestModel.objects(test_id=5)
        assert len(query1._where) == 1

        op = query1._where[0]

        assert isinstance(op, statements.WhereClause)
        assert isinstance(op.operator, operators.EqualsOperator)
        assert op.value == 5

        query2 = query1.filter(expected_result__gte=1)
        assert len(query2._where) == 2

        op = query2._where[1]
        self.assertIsInstance(op, statements.WhereClause)
        self.assertIsInstance(op.operator, operators.GreaterThanOrEqualOperator)
        assert op.value == 1

    def test_query_expression_parsing(self):
        """ Tests that query experessions are evaluated properly """
        query1 = TestModel.filter(TestModel.test_id == 5)
        assert len(query1._where) == 1

        op = query1._where[0]
        assert isinstance(op, statements.WhereClause)
        assert isinstance(op.operator, operators.EqualsOperator)
        assert op.value == 5

        query2 = query1.filter(TestModel.expected_result >= 1)
        assert len(query2._where) == 2

        op = query2._where[1]
        self.assertIsInstance(op, statements.WhereClause)
        self.assertIsInstance(op.operator, operators.GreaterThanOrEqualOperator)
        assert op.value == 1

    def test_using_invalid_column_names_in_filter_kwargs_raises_error(self):
        """
        Tests that using invalid or nonexistant column names for filter args raises an error
        """
        with self.assertRaises(query.QueryException):
            TestModel.objects(nonsense=5)

    def test_using_nonexistant_column_names_in_query_args_raises_error(self):
        """
        Tests that using invalid or nonexistant columns for query args raises an error
        """
        with self.assertRaises(AttributeError):
            TestModel.objects(TestModel.nonsense == 5)

    def test_using_non_query_operators_in_query_args_raises_error(self):
        """
        Tests that providing query args that are not query operator instances raises an error
        """
        with self.assertRaises(query.QueryException):
            TestModel.objects(5)

    def test_queryset_is_immutable(self):
        """
        Tests that calling a queryset function that changes it's state returns a new queryset
        """
        query1 = TestModel.objects(test_id=5)
        assert len(query1._where) == 1

        query2 = query1.filter(expected_result__gte=1)
        assert len(query2._where) == 2
        assert len(query1._where) == 1

    def test_queryset_limit_immutability(self):
        """
        Tests that calling a queryset function that changes it's state returns a new queryset with same limit
        """
        query1 = TestModel.objects(test_id=5).limit(1)
        assert query1._limit == 1

        query2 = query1.filter(expected_result__gte=1)
        assert query2._limit == 1

        query3 = query1.filter(expected_result__gte=1).limit(2)
        assert query1._limit == 1
        assert query3._limit == 2

    def test_the_all_method_duplicates_queryset(self):
        """
        Tests that calling all on a queryset with previously defined filters duplicates queryset
        """
        query1 = TestModel.objects(test_id=5)
        assert len(query1._where) == 1

        query2 = query1.filter(expected_result__gte=1)
        assert len(query2._where) == 2

        query3 = query2.all()
        assert query3 == query2

    def test_queryset_with_distinct(self):
        """
        Tests that calling distinct on a queryset w/without parameter are evaluated properly.
        """

        query1 = TestModel.objects.distinct()
        self.assertEqual(len(query1._distinct_fields), 1)

        query2 = TestModel.objects.distinct(['test_id'])
        self.assertEqual(len(query2._distinct_fields), 1)

        query3 = TestModel.objects.distinct(['test_id', 'attempt_id'])
        self.assertEqual(len(query3._distinct_fields), 2)

    def test_defining_only_fields(self):
        """
        Tests defining only fields

        @since 3.5
        @jira_ticket PYTHON-560
        @expected_result deferred fields should not be returned

        @test_category object_mapper
        """
        # simple only definition
        q = TestModel.objects.only(['attempt_id', 'description'])
        self.assertEqual(q._select_fields(), ['attempt_id', 'description'])

        with self.assertRaises(query.QueryException):
            TestModel.objects.only(['nonexistent_field'])

        # Cannot define more than once only fields
        with self.assertRaises(query.QueryException):
            TestModel.objects.only(['description']).only(['attempt_id'])

        # only with defer fields
        q = TestModel.objects.only(['attempt_id', 'description'])
        q = q.defer(['description'])
        self.assertEqual(q._select_fields(), ['attempt_id'])

        # Eliminate all results confirm exception is thrown
        q = TestModel.objects.only(['description'])
        q = q.defer(['description'])
        with self.assertRaises(query.QueryException):
            q._select_fields()

        q = TestModel.objects.filter(test_id=0).only(['test_id', 'attempt_id', 'description'])
        self.assertEqual(q._select_fields(), ['attempt_id', 'description'])

        # no fields to select
        with self.assertRaises(query.QueryException):
            q = TestModel.objects.only(['test_id']).defer(['test_id'])
            q._select_fields()

        with self.assertRaises(query.QueryException):
            q = TestModel.objects.filter(test_id=0).only(['test_id'])
            q._select_fields()

    def test_defining_defer_fields(self):
        """
        Tests defining defer fields

        @since 3.5
        @jira_ticket PYTHON-560
        @jira_ticket PYTHON-599
        @expected_result deferred fields should not be returned

        @test_category object_mapper
        """

        # simple defer definition
        q = TestModel.objects.defer(['attempt_id', 'description'])
        self.assertEqual(q._select_fields(), ['test_id', 'expected_result', 'test_result'])

        with self.assertRaises(query.QueryException):
            TestModel.objects.defer(['nonexistent_field'])

        # defer more than one
        q = TestModel.objects.defer(['attempt_id', 'description'])
        q = q.defer(['expected_result'])
        self.assertEqual(q._select_fields(), ['test_id', 'test_result'])

        # defer with only
        q = TestModel.objects.defer(['description', 'attempt_id'])
        q = q.only(['description', 'test_id'])
        self.assertEqual(q._select_fields(), ['test_id'])

        # Eliminate all results confirm exception is thrown
        q = TestModel.objects.defer(['description', 'attempt_id'])
        q = q.only(['description'])
        with self.assertRaises(query.QueryException):
            q._select_fields()

        # implicit defer
        q = TestModel.objects.filter(test_id=0)
        self.assertEqual(q._select_fields(), ['attempt_id', 'description', 'expected_result', 'test_result'])

        # when all fields are defered, it fallbacks select the partition keys
        q = TestModel.objects.defer(['test_id', 'attempt_id', 'description', 'expected_result', 'test_result'])
        self.assertEqual(q._select_fields(), ['test_id'])


class BaseQuerySetUsage(BaseCassEngTestCase):

    @classmethod
    def setUpClass(cls):
        super(BaseQuerySetUsage, cls).setUpClass()
        drop_table(TestModel)
        drop_table(IndexedTestModel)

        sync_table(TestModel)
        sync_table(IndexedTestModel)
        sync_table(TestMultiClusteringModel)

        TestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30)
        TestModel.objects.create(test_id=0, attempt_id=1, description='try2', expected_result=10, test_result=30)
        TestModel.objects.create(test_id=0, attempt_id=2, description='try3', expected_result=15, test_result=30)
        TestModel.objects.create(test_id=0, attempt_id=3, description='try4', expected_result=20, test_result=25)

        TestModel.objects.create(test_id=1, attempt_id=0, description='try5', expected_result=5, test_result=25)
        TestModel.objects.create(test_id=1, attempt_id=1, description='try6', expected_result=10, test_result=25)
        TestModel.objects.create(test_id=1, attempt_id=2, description='try7', expected_result=15, test_result=25)
        TestModel.objects.create(test_id=1, attempt_id=3, description='try8', expected_result=20, test_result=20)

        TestModel.objects.create(test_id=2, attempt_id=0, description='try9', expected_result=50, test_result=40)
        TestModel.objects.create(test_id=2, attempt_id=1, description='try10', expected_result=60, test_result=40)
        TestModel.objects.create(test_id=2, attempt_id=2, description='try11', expected_result=70, test_result=45)
        TestModel.objects.create(test_id=2, attempt_id=3, description='try12', expected_result=75, test_result=45)

        IndexedTestModel.objects.create(test_id=0, attempt_id=0, description='try1', expected_result=5, test_result=30)
        IndexedTestModel.objects.create(test_id=1, attempt_id=1, description='try2', expected_result=10, test_result=30)
        IndexedTestModel.objects.create(test_id=2, attempt_id=2, description='try3', expected_result=15, test_result=30)
        IndexedTestModel.objects.create(test_id=3, attempt_id=3, description='try4', expected_result=20, test_result=25)

        IndexedTestModel.objects.create(test_id=4, attempt_id=0, description='try5', expected_result=5, test_result=25)
        IndexedTestModel.objects.create(test_id=5, attempt_id=1, description='try6', expected_result=10, test_result=25)
        IndexedTestModel.objects.create(test_id=6, attempt_id=2, description='try7', expected_result=15, test_result=25)
        IndexedTestModel.objects.create(test_id=7, attempt_id=3, description='try8', expected_result=20, test_result=20)

        IndexedTestModel.objects.create(test_id=8, attempt_id=0, description='try9', expected_result=50, test_result=40)
        IndexedTestModel.objects.create(test_id=9, attempt_id=1, description='try10', expected_result=60,
                                        test_result=40)
        IndexedTestModel.objects.create(test_id=10, attempt_id=2, description='try11', expected_result=70,
                                        test_result=45)
        IndexedTestModel.objects.create(test_id=11, attempt_id=3, description='try12', expected_result=75,
                                        test_result=45)

        if CASSANDRA_VERSION >= Version('2.1'):
            drop_table(IndexedCollectionsTestModel)
            sync_table(IndexedCollectionsTestModel)
            IndexedCollectionsTestModel.objects.create(test_id=12, attempt_id=3, description='list12', expected_result=75,
                                                       test_result=45, test_list=[1, 2, 42], test_set=set([1, 2, 3]),
                                                       test_map={'1': 1, '2': 2, '3': 3})
            IndexedCollectionsTestModel.objects.create(test_id=13, attempt_id=3, description='list13', expected_result=75,
                                                       test_result=45, test_list=[3, 4, 5], test_set=set([4, 5, 42]),
                                                       test_map={'1': 5, '2': 6, '3': 7})
            IndexedCollectionsTestModel.objects.create(test_id=14, attempt_id=3, description='list14', expected_result=75,
                                                       test_result=45, test_list=[1, 2, 3], test_set=set([1, 2, 3]),
                                                       test_map={'1': 1, '2': 2, '3': 42})

            IndexedCollectionsTestModel.objects.create(test_id=15, attempt_id=4, description='list14', expected_result=75,
                                                       test_result=45, test_list_no_index=[1, 2, 3], test_set_no_index=set([1, 2, 3]),
                                                       test_map_no_index={'1': 1, '2': 2, '3': 42})

    @classmethod
    def tearDownClass(cls):
        super(BaseQuerySetUsage, cls).tearDownClass()
        drop_table(TestModel)
        drop_table(IndexedTestModel)
        drop_table(TestMultiClusteringModel)


class TestQuerySetCountSelectionAndIteration(BaseQuerySetUsage):

    @execute_count(2)
    def test_count(self):
        """ Tests that adding filtering statements affects the count query as expected """
        assert TestModel.objects.count() == 12

        q = TestModel.objects(test_id=0)
        assert q.count() == 4

    @execute_count(2)
    def test_query_expression_count(self):
        """ Tests that adding query statements affects the count query as expected """
        assert TestModel.objects.count() == 12

        q = TestModel.objects(TestModel.test_id == 0)
        assert q.count() == 4

    @execute_count(3)
    def test_iteration(self):
        """ Tests that iterating over a query set pulls back all of the expected results """
        q = TestModel.objects(test_id=0)
        # tuple of expected attempt_id, expected_result values
        compare_set = set([(0, 5), (1, 10), (2, 15), (3, 20)])
        for t in q:
            val = t.attempt_id, t.expected_result
            assert val in compare_set
            compare_set.remove(val)
        assert len(compare_set) == 0

        # test with regular filtering
        q = TestModel.objects(attempt_id=3).allow_filtering()
        assert len(q) == 3
        # tuple of expected test_id, expected_result values
        compare_set = set([(0, 20), (1, 20), (2, 75)])
        for t in q:
            val = t.test_id, t.expected_result
            assert val in compare_set
            compare_set.remove(val)
        assert len(compare_set) == 0

        # test with query method
        q = TestModel.objects(TestModel.attempt_id == 3).allow_filtering()
        assert len(q) == 3
        # tuple of expected test_id, expected_result values
        compare_set = set([(0, 20), (1, 20), (2, 75)])
        for t in q:
            val = t.test_id, t.expected_result
            assert val in compare_set
            compare_set.remove(val)
        assert len(compare_set) == 0

    @execute_count(2)
    def test_multiple_iterations_work_properly(self):
        """ Tests that iterating over a query set more than once works """
        # test with both the filtering method and the query method
        for q in (TestModel.objects(test_id=0), TestModel.objects(TestModel.test_id == 0)):
            # tuple of expected attempt_id, expected_result values
            compare_set = set([(0, 5), (1, 10), (2, 15), (3, 20)])
            for t in q:
                val = t.attempt_id, t.expected_result
                assert val in compare_set
                compare_set.remove(val)
            assert len(compare_set) == 0

            # try it again
            compare_set = set([(0, 5), (1, 10), (2, 15), (3, 20)])
            for t in q:
                val = t.attempt_id, t.expected_result
                assert val in compare_set
                compare_set.remove(val)
            assert len(compare_set) == 0

    @execute_count(2)
    def test_multiple_iterators_are_isolated(self):
        """
        tests that the use of one iterator does not affect the behavior of another
        """
        for q in (TestModel.objects(test_id=0), TestModel.objects(TestModel.test_id == 0)):
            q = q.order_by('attempt_id')
            expected_order = [0, 1, 2, 3]
            iter1 = iter(q)
            iter2 = iter(q)
            for attempt_id in expected_order:
                assert next(iter1).attempt_id == attempt_id
                assert next(iter2).attempt_id == attempt_id

    @execute_count(3)
    def test_get_success_case(self):
        """
        Tests that the .get() method works on new and existing querysets
        """
        m = TestModel.objects.get(test_id=0, attempt_id=0)
        assert isinstance(m, TestModel)
        assert m.test_id == 0
        assert m.attempt_id == 0

        q = TestModel.objects(test_id=0, attempt_id=0)
        m = q.get()
        assert isinstance(m, TestModel)
        assert m.test_id == 0
        assert m.attempt_id == 0

        q = TestModel.objects(test_id=0)
        m = q.get(attempt_id=0)
        assert isinstance(m, TestModel)
        assert m.test_id == 0
        assert m.attempt_id == 0

    @execute_count(3)
    def test_query_expression_get_success_case(self):
        """
        Tests that the .get() method works on new and existing querysets
        """
        m = TestModel.get(TestModel.test_id == 0, TestModel.attempt_id == 0)
        assert isinstance(m, TestModel)
        assert m.test_id == 0
        assert m.attempt_id == 0

        q = TestModel.objects(TestModel.test_id == 0, TestModel.attempt_id == 0)
        m = q.get()
        assert isinstance(m, TestModel)
        assert m.test_id == 0
        assert m.attempt_id == 0

        q = TestModel.objects(TestModel.test_id == 0)
        m = q.get(TestModel.attempt_id == 0)
        assert isinstance(m, TestModel)
        assert m.test_id == 0
        assert m.attempt_id == 0

    @execute_count(1)
    def test_get_doesnotexist_exception(self):
        """
        Tests that get calls that don't return a result raises a DoesNotExist error
        """
        with self.assertRaises(TestModel.DoesNotExist):
            TestModel.objects.get(test_id=100)

    @execute_count(1)
    def test_get_multipleobjects_exception(self):
        """
        Tests that get calls that return multiple results raise a MultipleObjectsReturned error
        """
        with self.assertRaises(TestModel.MultipleObjectsReturned):
            TestModel.objects.get(test_id=1)

    def test_allow_filtering_flag(self):
        """
        """

@execute_count(4)
def test_non_quality_filtering():
    class NonEqualityFilteringModel(Model):

        example_id = columns.UUID(primary_key=True, default=uuid.uuid4)
        sequence_id = columns.Integer(primary_key=True)  # sequence_id is a clustering key
        example_type = columns.Integer(index=True)
        created_at = columns.DateTime()

    drop_table(NonEqualityFilteringModel)
    sync_table(NonEqualityFilteringModel)

    # setup table, etc.

    NonEqualityFilteringModel.create(sequence_id=1, example_type=0, created_at=datetime.now())
    NonEqualityFilteringModel.create(sequence_id=3, example_type=0, created_at=datetime.now())
    NonEqualityFilteringModel.create(sequence_id=5, example_type=1, created_at=datetime.now())

    qa = NonEqualityFilteringModel.objects(NonEqualityFilteringModel.sequence_id > 3).allow_filtering()
    num = qa.count()
    assert num == 1, num


class TestQuerySetDistinct(BaseQuerySetUsage):

    @execute_count(1)
    def test_distinct_without_parameter(self):
        q = TestModel.objects.distinct()
        self.assertEqual(len(q), 3)

    @execute_count(1)
    def test_distinct_with_parameter(self):
        q = TestModel.objects.distinct(['test_id'])
        self.assertEqual(len(q), 3)

    @execute_count(1)
    def test_distinct_with_filter(self):
        q = TestModel.objects.distinct(['test_id']).filter(test_id__in=[1, 2])
        self.assertEqual(len(q), 2)

    @execute_count(1)
    def test_distinct_with_non_partition(self):
        with self.assertRaises(InvalidRequest):
            q = TestModel.objects.distinct(['description']).filter(test_id__in=[1, 2])
            len(q)

    @execute_count(1)
    def test_zero_result(self):
        q = TestModel.objects.distinct(['test_id']).filter(test_id__in=[52])
        self.assertEqual(len(q), 0)

    @greaterthancass21
    @execute_count(2)
    def test_distinct_with_explicit_count(self):
        q = TestModel.objects.distinct(['test_id'])
        self.assertEqual(q.count(), 3)

        q = TestModel.objects.distinct(['test_id']).filter(test_id__in=[1, 2])
        self.assertEqual(q.count(), 2)


class TestQuerySetOrdering(BaseQuerySetUsage):
    @execute_count(2)
    def test_order_by_success_case(self):
        q = TestModel.objects(test_id=0).order_by('attempt_id')
        expected_order = [0, 1, 2, 3]
        for model, expect in zip(q, expected_order):
            assert model.attempt_id == expect

        q = q.order_by('-attempt_id')
        expected_order.reverse()
        for model, expect in zip(q, expected_order):
            assert model.attempt_id == expect

    def test_ordering_by_non_second_primary_keys_fail(self):
        # kwarg filtering
        with self.assertRaises(query.QueryException):
            TestModel.objects(test_id=0).order_by('test_id')

        # kwarg filtering
        with self.assertRaises(query.QueryException):
            TestModel.objects(TestModel.test_id == 0).order_by('test_id')

    def test_ordering_by_non_primary_keys_fails(self):
        with self.assertRaises(query.QueryException):
            TestModel.objects(test_id=0).order_by('description')

    def test_ordering_on_indexed_columns_fails(self):
        with self.assertRaises(query.QueryException):
            IndexedTestModel.objects(test_id=0).order_by('attempt_id')

    @execute_count(8)
    def test_ordering_on_multiple_clustering_columns(self):
        TestMultiClusteringModel.create(one=1, two=1, three=4)
        TestMultiClusteringModel.create(one=1, two=1, three=2)
        TestMultiClusteringModel.create(one=1, two=1, three=5)
        TestMultiClusteringModel.create(one=1, two=1, three=1)
        TestMultiClusteringModel.create(one=1, two=1, three=3)

        results = TestMultiClusteringModel.objects.filter(one=1, two=1).order_by('-two', '-three')
        assert [r.three for r in results] == [5, 4, 3, 2, 1]

        results = TestMultiClusteringModel.objects.filter(one=1, two=1).order_by('two', 'three')
        assert [r.three for r in results] == [1, 2, 3, 4, 5]

        results = TestMultiClusteringModel.objects.filter(one=1, two=1).order_by('two').order_by('three')
        assert [r.three for r in results] == [1, 2, 3, 4, 5]


class TestQuerySetSlicing(BaseQuerySetUsage):

    @execute_count(1)
    def test_out_of_range_index_raises_error(self):
        q = TestModel.objects(test_id=0).order_by('attempt_id')
        with self.assertRaises(IndexError):
            q[10]

    @execute_count(1)
    def test_array_indexing_works_properly(self):
        q = TestModel.objects(test_id=0).order_by('attempt_id')
        expected_order = [0, 1, 2, 3]
        for i in range(len(q)):
            assert q[i].attempt_id == expected_order[i]

    @execute_count(1)
    def test_negative_indexing_works_properly(self):
        q = TestModel.objects(test_id=0).order_by('attempt_id')
        expected_order = [0, 1, 2, 3]
        assert q[-1].attempt_id == expected_order[-1]
        assert q[-2].attempt_id == expected_order[-2]

    @execute_count(1)
    def test_slicing_works_properly(self):
        q = TestModel.objects(test_id=0).order_by('attempt_id')
        expected_order = [0, 1, 2, 3]

        for model, expect in zip(q[1:3], expected_order[1:3]):
            self.assertEqual(model.attempt_id, expect)

        for model, expect in zip(q[0:3:2], expected_order[0:3:2]):
            self.assertEqual(model.attempt_id, expect)

    @execute_count(1)
    def test_negative_slicing(self):
        q = TestModel.objects(test_id=0).order_by('attempt_id')
        expected_order = [0, 1, 2, 3]

        for model, expect in zip(q[-3:], expected_order[-3:]):
            self.assertEqual(model.attempt_id, expect)

        for model, expect in zip(q[:-1], expected_order[:-1]):
            self.assertEqual(model.attempt_id, expect)

        for model, expect in zip(q[1:-1], expected_order[1:-1]):
            self.assertEqual(model.attempt_id, expect)

        for model, expect in zip(q[-3:-1], expected_order[-3:-1]):
            self.assertEqual(model.attempt_id, expect)

        for model, expect in zip(q[-3:-1:2], expected_order[-3:-1:2]):
            self.assertEqual(model.attempt_id, expect)


class TestQuerySetValidation(BaseQuerySetUsage):

    def test_primary_key_or_index_must_be_specified(self):
        """
        Tests that queries that don't have an equals relation to a primary key or indexed field fail
        """
        with self.assertRaises(query.QueryException):
            q = TestModel.objects(test_result=25)
            list([i for i in q])

    def test_primary_key_or_index_must_have_equal_relation_filter(self):
        """
        Tests that queries that don't have non equal (>,<, etc) relation to a primary key or indexed field fail
        """
        with self.assertRaises(query.QueryException):
            q = TestModel.objects(test_id__gt=0)
            list([i for i in q])

    @greaterthancass20
    @execute_count(7)
    def test_indexed_field_can_be_queried(self):
        """
        Tests that queries on an indexed field will work without any primary key relations specified
        """
        q = IndexedTestModel.objects(test_result=25)
        self.assertEqual(q.count(), 4)

        q = IndexedCollectionsTestModel.objects.filter(test_list__contains=42)
        self.assertEqual(q.count(), 1)

        q = IndexedCollectionsTestModel.objects.filter(test_list__contains=13)
        self.assertEqual(q.count(), 0)

        q = IndexedCollectionsTestModel.objects.filter(test_set__contains=42)
        self.assertEqual(q.count(), 1)

        q = IndexedCollectionsTestModel.objects.filter(test_set__contains=13)
        self.assertEqual(q.count(), 0)

        q = IndexedCollectionsTestModel.objects.filter(test_map__contains=42)
        self.assertEqual(q.count(), 1)

        q = IndexedCollectionsTestModel.objects.filter(test_map__contains=13)
        self.assertEqual(q.count(), 0)


class TestQuerySetDelete(BaseQuerySetUsage):

    @execute_count(9)
    def test_delete(self):
        TestModel.objects.create(test_id=3, attempt_id=0, description='try9', expected_result=50, test_result=40)
        TestModel.objects.create(test_id=3, attempt_id=1, description='try10', expected_result=60, test_result=40)
        TestModel.objects.create(test_id=3, attempt_id=2, description='try11', expected_result=70, test_result=45)
        TestModel.objects.create(test_id=3, attempt_id=3, description='try12', expected_result=75, test_result=45)

        assert TestModel.objects.count() == 16
        assert TestModel.objects(test_id=3).count() == 4

        TestModel.objects(test_id=3).delete()

        assert TestModel.objects.count() == 12
        assert TestModel.objects(test_id=3).count() == 0

    def test_delete_without_partition_key(self):
        """ Tests that attempting to delete a model without defining a partition key fails """
        with self.assertRaises(query.QueryException):
            TestModel.objects(attempt_id=0).delete()

    def test_delete_without_any_where_args(self):
        """ Tests that attempting to delete a whole table without any arguments will fail """
        with self.assertRaises(query.QueryException):
            TestModel.objects(attempt_id=0).delete()

    @greaterthanorequalcass30
    @execute_count(18)
    def test_range_deletion(self):
        """
        Tests that range deletion work as expected
        """

        for i in range(10):
            TestMultiClusteringModel.objects().create(one=1, two=i, three=i)

        TestMultiClusteringModel.objects(one=1, two__gte=0, two__lte=3).delete()
        self.assertEqual(6, len(TestMultiClusteringModel.objects.all()))

        TestMultiClusteringModel.objects(one=1, two__gt=3, two__lt=5).delete()
        self.assertEqual(5, len(TestMultiClusteringModel.objects.all()))

        TestMultiClusteringModel.objects(one=1, two__in=[8, 9]).delete()
        self.assertEqual(3, len(TestMultiClusteringModel.objects.all()))

        TestMultiClusteringModel.objects(one__in=[1], two__gte=0).delete()
        self.assertEqual(0, len(TestMultiClusteringModel.objects.all()))


class TimeUUIDQueryModel(Model):

    partition = columns.UUID(primary_key=True)
    time = columns.TimeUUID(primary_key=True)
    data = columns.Text(required=False)


class TestMinMaxTimeUUIDFunctions(BaseCassEngTestCase):
    @classmethod
    def setUpClass(cls):
        super(TestMinMaxTimeUUIDFunctions, cls).setUpClass()
        sync_table(TimeUUIDQueryModel)

    @classmethod
    def tearDownClass(cls):
        super(TestMinMaxTimeUUIDFunctions, cls).tearDownClass()
        drop_table(TimeUUIDQueryModel)

    @execute_count(7)
    def test_tzaware_datetime_support(self):
        """Test that using timezone aware datetime instances works with the
        MinTimeUUID/MaxTimeUUID functions.
        """
        pk = uuid4()
        midpoint_utc = datetime.utcnow().replace(tzinfo=TzOffset(0))
        midpoint_helsinki = midpoint_utc.astimezone(TzOffset(3))

        # Assert pre-condition that we have the same logical point in time
        assert midpoint_utc.utctimetuple() == midpoint_helsinki.utctimetuple()
        assert midpoint_utc.timetuple() != midpoint_helsinki.timetuple()

        TimeUUIDQueryModel.create(
            partition=pk,
            time=uuid_from_time(midpoint_utc - timedelta(minutes=1)),
            data='1')

        TimeUUIDQueryModel.create(
            partition=pk,
            time=uuid_from_time(midpoint_utc),
            data='2')

        TimeUUIDQueryModel.create(
            partition=pk,
            time=uuid_from_time(midpoint_utc + timedelta(minutes=1)),
            data='3')

        assert ['1', '2'] == [o.data for o in TimeUUIDQueryModel.filter(
            TimeUUIDQueryModel.partition == pk,
            TimeUUIDQueryModel.time <= functions.MaxTimeUUID(midpoint_utc))]

        assert ['1', '2'] == [o.data for o in TimeUUIDQueryModel.filter(
            TimeUUIDQueryModel.partition == pk,
            TimeUUIDQueryModel.time <= functions.MaxTimeUUID(midpoint_helsinki))]

        assert ['2', '3'] == [o.data for o in TimeUUIDQueryModel.filter(
            TimeUUIDQueryModel.partition == pk,
            TimeUUIDQueryModel.time >= functions.MinTimeUUID(midpoint_utc))]

        assert ['2', '3'] == [o.data for o in TimeUUIDQueryModel.filter(
            TimeUUIDQueryModel.partition == pk,
            TimeUUIDQueryModel.time >= functions.MinTimeUUID(midpoint_helsinki))]

    @execute_count(8)
    def test_success_case(self):
        """ Test that the min and max time uuid functions work as expected """
        pk = uuid4()
        startpoint = datetime.utcnow()
        TimeUUIDQueryModel.create(partition=pk, time=uuid_from_time(startpoint + timedelta(seconds=1)), data='1')
        TimeUUIDQueryModel.create(partition=pk, time=uuid_from_time(startpoint + timedelta(seconds=2)), data='2')
        midpoint = startpoint + timedelta(seconds=3)
        TimeUUIDQueryModel.create(partition=pk, time=uuid_from_time(startpoint + timedelta(seconds=4)), data='3')
        TimeUUIDQueryModel.create(partition=pk, time=uuid_from_time(startpoint + timedelta(seconds=5)), data='4')

        # test kwarg filtering
        q = TimeUUIDQueryModel.filter(partition=pk, time__lte=functions.MaxTimeUUID(midpoint))
        q = [d for d in q]
        self.assertEqual(len(q), 2, msg="Got: %s" % q)
        datas = [d.data for d in q]
        assert '1' in datas
        assert '2' in datas

        q = TimeUUIDQueryModel.filter(partition=pk, time__gte=functions.MinTimeUUID(midpoint))
        assert len(q) == 2
        datas = [d.data for d in q]
        assert '3' in datas
        assert '4' in datas

        # test query expression filtering
        q = TimeUUIDQueryModel.filter(
            TimeUUIDQueryModel.partition == pk,
            TimeUUIDQueryModel.time <= functions.MaxTimeUUID(midpoint)
        )
        q = [d for d in q]
        assert len(q) == 2
        datas = [d.data for d in q]
        assert '1' in datas
        assert '2' in datas

        q = TimeUUIDQueryModel.filter(
            TimeUUIDQueryModel.partition == pk,
            TimeUUIDQueryModel.time >= functions.MinTimeUUID(midpoint)
        )
        assert len(q) == 2
        datas = [d.data for d in q]
        assert '3' in datas
        assert '4' in datas


class TestInOperator(BaseQuerySetUsage):
    @execute_count(1)
    def test_kwarg_success_case(self):
        """ Tests the in operator works with the kwarg query method """
        q = TestModel.filter(test_id__in=[0, 1])
        assert q.count() == 8

    @execute_count(1)
    def test_query_expression_success_case(self):
        """ Tests the in operator works with the query expression query method """
        q = TestModel.filter(TestModel.test_id.in_([0, 1]))
        assert q.count() == 8

    @execute_count(5)
    def test_bool(self):
        """
        Adding coverage to cqlengine for bool types.

        @since 3.6
        @jira_ticket PYTHON-596
        @expected_result bool results should be filtered appropriately

        @test_category object_mapper
        """
        class bool_model(Model):
            k = columns.Integer(primary_key=True)
            b = columns.Boolean(primary_key=True)
            v = columns.Integer(default=3)
        sync_table(bool_model)

        bool_model.create(k=0, b=True)
        bool_model.create(k=0, b=False)
        self.assertEqual(len(bool_model.objects.all()), 2)
        self.assertEqual(len(bool_model.objects.filter(k=0, b=True)), 1)
        self.assertEqual(len(bool_model.objects.filter(k=0, b=False)), 1)

    @execute_count(3)
    def test_bool_filter(self):
        """
        Test to ensure that we don't translate boolean objects to String unnecessarily in filter clauses

        @since 3.6
        @jira_ticket PYTHON-596
        @expected_result We should not receive a server error

        @test_category object_mapper
        """
        class bool_model2(Model):
            k = columns.Boolean(primary_key=True)
            b = columns.Integer(primary_key=True)
            v = columns.Text()
        drop_table(bool_model2)
        sync_table(bool_model2)

        bool_model2.create(k=True, b=1, v='a')
        bool_model2.create(k=False, b=1, v='b')
        self.assertEqual(len(list(bool_model2.objects(k__in=(True, False)))), 2)


@greaterthancass20
class TestContainsOperator(BaseQuerySetUsage):

    @execute_count(6)
    def test_kwarg_success_case(self):
        """ Tests the CONTAINS operator works with the kwarg query method """
        q = IndexedCollectionsTestModel.filter(test_list__contains=1)
        self.assertEqual(q.count(), 2)

        q = IndexedCollectionsTestModel.filter(test_list__contains=13)
        self.assertEqual(q.count(), 0)

        q = IndexedCollectionsTestModel.filter(test_set__contains=3)
        self.assertEqual(q.count(), 2)

        q = IndexedCollectionsTestModel.filter(test_set__contains=13)
        self.assertEqual(q.count(), 0)

        q = IndexedCollectionsTestModel.filter(test_map__contains=42)
        self.assertEqual(q.count(), 1)

        q = IndexedCollectionsTestModel.filter(test_map__contains=13)
        self.assertEqual(q.count(), 0)

        with self.assertRaises(QueryException):
            q = IndexedCollectionsTestModel.filter(test_list_no_index__contains=1)
            self.assertEqual(q.count(), 0)
        with self.assertRaises(QueryException):
            q = IndexedCollectionsTestModel.filter(test_set_no_index__contains=1)
            self.assertEqual(q.count(), 0)
        with self.assertRaises(QueryException):
            q = IndexedCollectionsTestModel.filter(test_map_no_index__contains=1)
            self.assertEqual(q.count(), 0)

    @execute_count(6)
    def test_query_expression_success_case(self):
        """ Tests the CONTAINS operator works with the query expression query method """
        q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_list.contains_(1))
        self.assertEqual(q.count(), 2)

        q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_list.contains_(13))
        self.assertEqual(q.count(), 0)

        q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_set.contains_(3))
        self.assertEqual(q.count(), 2)

        q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_set.contains_(13))
        self.assertEqual(q.count(), 0)

        q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_map.contains_(42))
        self.assertEqual(q.count(), 1)

        q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_map.contains_(13))
        self.assertEqual(q.count(), 0)

        with self.assertRaises(QueryException):
            q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_map_no_index.contains_(1))
            self.assertEqual(q.count(), 0)
        with self.assertRaises(QueryException):
            q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_map_no_index.contains_(1))
            self.assertEqual(q.count(), 0)
        with self.assertRaises(QueryException):
            q = IndexedCollectionsTestModel.filter(IndexedCollectionsTestModel.test_map_no_index.contains_(1))
            self.assertEqual(q.count(), 0)


class TestValuesList(BaseQuerySetUsage):

    @execute_count(2)
    def test_values_list(self):
        q = TestModel.objects.filter(test_id=0, attempt_id=1)
        item = q.values_list('test_id', 'attempt_id', 'description', 'expected_result', 'test_result').first()
        assert item == [0, 1, 'try2', 10, 30]

        item = q.values_list('expected_result', flat=True).first()
        assert item == 10


class TestObjectsProperty(BaseQuerySetUsage):
    @execute_count(1)
    def test_objects_property_returns_fresh_queryset(self):
        assert TestModel.objects._result_cache is None
        len(TestModel.objects)  # evaluate queryset
        assert TestModel.objects._result_cache is None


class PageQueryTests(BaseCassEngTestCase):
    @execute_count(3)
    def test_paged_result_handling(self):
        if PROTOCOL_VERSION < 2:
            raise unittest.SkipTest("Paging requires native protocol 2+, currently using: {0}".format(PROTOCOL_VERSION))

        # addresses #225
        class PagingTest(Model):
            id = columns.Integer(primary_key=True)
            val = columns.Integer()
        sync_table(PagingTest)

        PagingTest.create(id=1, val=1)
        PagingTest.create(id=2, val=2)

        session = get_session()
        with mock.patch.object(session, 'default_fetch_size', 1):
            results = PagingTest.objects()[:]

        assert len(results) == 2


class ModelQuerySetTimeoutTestCase(BaseQuerySetUsage):
    def test_default_timeout(self):
        with mock.patch.object(Session, 'execute') as mock_execute:
            list(TestModel.objects())
            self.assertEqual(mock_execute.call_args[-1]['timeout'], NOT_SET)

    def test_float_timeout(self):
        with mock.patch.object(Session, 'execute') as mock_execute:
            list(TestModel.objects().timeout(0.5))
            self.assertEqual(mock_execute.call_args[-1]['timeout'], 0.5)

    def test_none_timeout(self):
        with mock.patch.object(Session, 'execute') as mock_execute:
            list(TestModel.objects().timeout(None))
            self.assertEqual(mock_execute.call_args[-1]['timeout'], None)


class DMLQueryTimeoutTestCase(BaseQuerySetUsage):
    def setUp(self):
        self.model = TestModel(test_id=1, attempt_id=1, description='timeout test')
        super(DMLQueryTimeoutTestCase, self).setUp()

    def test_default_timeout(self):
        with mock.patch.object(Session, 'execute') as mock_execute:
            self.model.save()
            self.assertEqual(mock_execute.call_args[-1]['timeout'], NOT_SET)

    def test_float_timeout(self):
        with mock.patch.object(Session, 'execute') as mock_execute:
            self.model.timeout(0.5).save()
            self.assertEqual(mock_execute.call_args[-1]['timeout'], 0.5)

    def test_none_timeout(self):
        with mock.patch.object(Session, 'execute') as mock_execute:
            self.model.timeout(None).save()
            self.assertEqual(mock_execute.call_args[-1]['timeout'], None)

    def test_timeout_then_batch(self):
        b = query.BatchQuery()
        m = self.model.timeout(None)
        with self.assertRaises(AssertionError):
            m.batch(b)

    def test_batch_then_timeout(self):
        b = query.BatchQuery()
        m = self.model.batch(b)
        with self.assertRaises(AssertionError):
            m.timeout(0.5)


class DBFieldModel(Model):
    k0 = columns.Integer(partition_key=True, db_field='a')
    k1 = columns.Integer(partition_key=True, db_field='b')
    c0 = columns.Integer(primary_key=True, db_field='c')
    v0 = columns.Integer(db_field='d')
    v1 = columns.Integer(db_field='e', index=True)


class DBFieldModelMixed1(Model):
    k0 = columns.Integer(partition_key=True, db_field='a')
    k1 = columns.Integer(partition_key=True)
    c0 = columns.Integer(primary_key=True, db_field='c')
    v0 = columns.Integer(db_field='d')
    v1 = columns.Integer(index=True)


class DBFieldModelMixed2(Model):
    k0 = columns.Integer(partition_key=True)
    k1 = columns.Integer(partition_key=True, db_field='b')
    c0 = columns.Integer(primary_key=True)
    v0 = columns.Integer(db_field='d')
    v1 = columns.Integer(index=True, db_field='e')


class TestModelQueryWithDBField(BaseCassEngTestCase):

    def setUp(cls):
        super(TestModelQueryWithDBField, cls).setUpClass()
        cls.model_list = [DBFieldModel, DBFieldModelMixed1, DBFieldModelMixed2]
        for model in cls.model_list:
            sync_table(model)

    def tearDown(cls):
        super(TestModelQueryWithDBField, cls).tearDownClass()
        for model in cls.model_list:
            drop_table(model)

    @execute_count(33)
    def test_basic_crud(self):
        """
        Tests creation update and delete of object model queries that are using db_field mappings.

        @since 3.1
        @jira_ticket PYTHON-351
        @expected_result results are properly retrieved without errors

        @test_category object_mapper
        """
        for model in self.model_list:
            values = {'k0': 1, 'k1': 2, 'c0': 3, 'v0': 4, 'v1': 5}
            # create
            i = model.create(**values)
            i = model.objects(k0=i.k0, k1=i.k1).first()
            self.assertEqual(i, model(**values))

            # create
            values['v0'] = 101
            i.update(v0=values['v0'])
            i = model.objects(k0=i.k0, k1=i.k1).first()
            self.assertEqual(i, model(**values))

            # delete
            model.objects(k0=i.k0, k1=i.k1).delete()
            i = model.objects(k0=i.k0, k1=i.k1).first()
            self.assertIsNone(i)

            i = model.create(**values)
            i = model.objects(k0=i.k0, k1=i.k1).first()
            self.assertEqual(i, model(**values))
            i.delete()
            model.objects(k0=i.k0, k1=i.k1).delete()
            i = model.objects(k0=i.k0, k1=i.k1).first()
            self.assertIsNone(i)

    @execute_count(21)
    def test_slice(self):
        """
        Tests slice queries for object models that are using db_field mapping

        @since 3.1
        @jira_ticket PYTHON-351
        @expected_result results are properly retrieved without errors

        @test_category object_mapper
        """
        for model in self.model_list:
            values = {'k0': 1, 'k1': 3, 'c0': 3, 'v0': 4, 'v1': 5}
            clustering_values = range(3)
            for c in clustering_values:
                values['c0'] = c
                i = model.create(**values)

            self.assertEqual(model.objects(k0=i.k0, k1=i.k1).count(), len(clustering_values))
            self.assertEqual(model.objects(k0=i.k0, k1=i.k1, c0=i.c0).count(), 1)
            self.assertEqual(model.objects(k0=i.k0, k1=i.k1, c0__lt=i.c0).count(), len(clustering_values[:-1]))
            self.assertEqual(model.objects(k0=i.k0, k1=i.k1, c0__gt=0).count(), len(clustering_values[1:]))

    @execute_count(15)
    def test_order(self):
        """
        Tests order by queries for object models that are using db_field mapping

        @since 3.1
        @jira_ticket PYTHON-351
        @expected_result results are properly retrieved without errors

        @test_category object_mapper
        """
        for model in self.model_list:
            values = {'k0': 1, 'k1': 4, 'c0': 3, 'v0': 4, 'v1': 5}
            clustering_values = range(3)
            for c in clustering_values:
                values['c0'] = c
                i = model.create(**values)
            self.assertEqual(model.objects(k0=i.k0, k1=i.k1).order_by('c0').first().c0, clustering_values[0])
            self.assertEqual(model.objects(k0=i.k0, k1=i.k1).order_by('-c0').first().c0, clustering_values[-1])

    @execute_count(15)
    def test_index(self):
        """
        Tests queries using index fields for object models using db_field mapping

        @since 3.1
        @jira_ticket PYTHON-351
        @expected_result results are properly retrieved without errors

        @test_category object_mapper
        """
        for model in self.model_list:
            values = {'k0': 1, 'k1': 5, 'c0': 3, 'v0': 4, 'v1': 5}
            clustering_values = range(3)
            for c in clustering_values:
                values['c0'] = c
                values['v1'] = c
                i = model.create(**values)
            self.assertEqual(model.objects(k0=i.k0, k1=i.k1).count(), len(clustering_values))
            self.assertEqual(model.objects(k0=i.k0, k1=i.k1, v1=0).count(), 1)

    @execute_count(1)
    def test_db_field_names_used(self):
        """
        Tests to ensure that with generated cql update statements correctly utilize the db_field values.

        @since 3.2
        @jira_ticket PYTHON-530
        @expected_result resulting cql_statements will use the db_field values

        @test_category object_mapper
        """

        values = ('k0', 'k1', 'c0', 'v0', 'v1')
        # Test QuerySet Path
        b = BatchQuery()
        DBFieldModel.objects(k0=1).batch(b).update(
            v0=0,
            v1=9,
        )
        for value in values:
            self.assertTrue(value not in str(b.queries[0]))

        # Test DML path
        b2 = BatchQuery()
        dml_field_model = DBFieldModel.create(k0=1, k1=5, c0=3, v0=4, v1=5)
        dml_field_model.batch(b2).update(
            v0=0,
            v1=9,
        )
        for value in values:
            self.assertTrue(value not in str(b2.queries[0]))

    def test_db_field_value_list(self):
        DBFieldModel.create(k0=0, k1=0, c0=0, v0=4, v1=5)

        self.assertEqual(DBFieldModel.objects.filter(c0=0, k0=0, k1=0).values_list('c0', 'v0')._defer_fields,
                         {'a', 'c', 'b'})
        self.assertEqual(DBFieldModel.objects.filter(c0=0, k0=0, k1=0).values_list('c0', 'v0')._only_fields,
                         ['c', 'd'])

        list(DBFieldModel.objects.filter(c0=0, k0=0, k1=0).values_list('c0', 'v0'))

class TestModelSmall(Model):

    test_id = columns.Integer(primary_key=True)


class TestModelQueryWithFetchSize(BaseCassEngTestCase):
    """
    Test FetchSize, and ensure that results are returned correctly
    regardless of the paging size

    @since 3.1
    @jira_ticket PYTHON-324
    @expected_result results are properly retrieved and the correct size

    @test_category object_mapper
    """

    @classmethod
    def setUpClass(cls):
        super(TestModelQueryWithFetchSize, cls).setUpClass()
        sync_table(TestModelSmall)

    @classmethod
    def tearDownClass(cls):
        super(TestModelQueryWithFetchSize, cls).tearDownClass()
        drop_table(TestModelSmall)

    @execute_count(9)
    def test_defaultFetchSize(self):
        with BatchQuery() as b:
            for i in range(5100):
                TestModelSmall.batch(b).create(test_id=i)
        self.assertEqual(len(TestModelSmall.objects.fetch_size(1)), 5100)
        self.assertEqual(len(TestModelSmall.objects.fetch_size(500)), 5100)
        self.assertEqual(len(TestModelSmall.objects.fetch_size(4999)), 5100)
        self.assertEqual(len(TestModelSmall.objects.fetch_size(5000)), 5100)
        self.assertEqual(len(TestModelSmall.objects.fetch_size(5001)), 5100)
        self.assertEqual(len(TestModelSmall.objects.fetch_size(5100)), 5100)
        self.assertEqual(len(TestModelSmall.objects.fetch_size(5101)), 5100)
        self.assertEqual(len(TestModelSmall.objects.fetch_size(1)), 5100)

        with self.assertRaises(QueryException):
            TestModelSmall.objects.fetch_size(0)
        with self.assertRaises(QueryException):
            TestModelSmall.objects.fetch_size(-1)


class People(Model):
    __table_name__ = "people"
    last_name = columns.Text(primary_key=True, partition_key=True)
    first_name = columns.Text(primary_key=True)
    birthday = columns.DateTime()


class People2(Model):
    __table_name__ = "people"
    last_name = columns.Text(primary_key=True, partition_key=True)
    first_name = columns.Text(primary_key=True)
    middle_name = columns.Text()
    birthday = columns.DateTime()


class TestModelQueryWithDifferedFeld(BaseCassEngTestCase):
    """
    Tests that selects with filter will deffer population of known values until after the results are returned.
    I.E.  Instead of generating SELECT * FROM People WHERE last_name="Smith" It will generate
    SELECT first_name, birthday FROM People WHERE last_name="Smith"
    Where last_name 'smith' will populated post query

    @since 3.2
    @jira_ticket PYTHON-520
    @expected_result only needed fields are included in the query

    @test_category object_mapper
    """
    @classmethod
    def setUpClass(cls):
        super(TestModelQueryWithDifferedFeld, cls).setUpClass()
        sync_table(People)

    @classmethod
    def tearDownClass(cls):
        super(TestModelQueryWithDifferedFeld, cls).tearDownClass()
        drop_table(People)

    @execute_count(8)
    def test_defaultFetchSize(self):
        # Populate Table
        People.objects.create(last_name="Smith", first_name="John", birthday=datetime.now())
        People.objects.create(last_name="Bestwater", first_name="Alan", birthday=datetime.now())
        People.objects.create(last_name="Smith", first_name="Greg", birthday=datetime.now())
        People.objects.create(last_name="Smith", first_name="Adam", birthday=datetime.now())

        # Check query constructions
        expected_fields = ['first_name', 'birthday']
        self.assertEqual(People.filter(last_name="Smith")._select_fields(), expected_fields)
        # Validate correct fields are fetched
        smiths = list(People.filter(last_name="Smith"))
        self.assertEqual(len(smiths), 3)
        self.assertTrue(smiths[0].last_name is not None)

        # Modify table with new value
        sync_table(People2)

        # populate new format
        People2.objects.create(last_name="Smith", first_name="Chris", middle_name="Raymond", birthday=datetime.now())
        People2.objects.create(last_name="Smith", first_name="Andrew", middle_name="Micheal", birthday=datetime.now())

        # validate query construction
        expected_fields = ['first_name', 'middle_name', 'birthday']
        self.assertEqual(People2.filter(last_name="Smith")._select_fields(), expected_fields)

        # validate correct items are returneds
        smiths = list(People2.filter(last_name="Smith"))
        self.assertEqual(len(smiths), 5)
        self.assertTrue(smiths[0].last_name is not None)
