import datetime

from save_to_db.core.scope import Scope
from save_to_db.core.item import Item
from save_to_db.utils.test_base import TestBase


class TestPersist(TestBase):

    ModelGeneralOne = None
    ModelGeneralTwo = None

    ModelAutoReverseOne = None
    ModelAutoReverseTwoA = None
    ModelAutoReverseTwoB = None
    ModelAutoReverseThreeA = None
    ModelAutoReverseThreeB = None
    ModelAutoReverseFourA = None
    ModelAutoReverseFourB = None

    def test_forward_and_reverse_relations_filters_one(self):
        # referencing parent and two

        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = [
                {"parent_1_1"},
                {"parent_x_1"},
                {"parent_x_x"},
                {"two_1_1"},
                {"two_1_x"},
                {"two_x_1"},
                {"two_x_x"},
                {"f_integer"},
            ]
            getters = [{"parent_1_1"}, {"parent_x_1"}, {"two_1_1"}, {"two_x_1"}]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = [{"f_integer"}]
            getters = [{"f_integer"}]

        persister = self.persister

        # parent_1_1
        item_parent_1_1 = ItemGeneralOne(f_string="parent_1_1", f_integer="101")
        item_child_1_1 = ItemGeneralOne(
            f_string="child_1_1", parent_1_1=item_parent_1_1
        )
        persister.persist(item_child_1_1)
        models = self.get_all_models(self.ModelGeneralOne)
        model = [m for m in models if m.f_string == "child_1_1"][0]
        self.assertEqual(model.parent_1_1.f_string, "parent_1_1")
        model = [m for m in models if m.f_string == "parent_1_1"][0]
        self.assertEqual(model.child_1_1.f_string, "child_1_1")

        # parent_x_1
        item_parent_x_1 = ItemGeneralOne(f_string="parent_x_1", f_integer="901")
        item_child_1_x = ItemGeneralOne(
            f_string="child_1_x", parent_x_1=item_parent_x_1
        )
        persister.persist(item_child_1_x)
        models = self.get_all_models(self.ModelGeneralOne)
        model = [m for m in models if m.f_string == "child_1_x"][0]
        self.assertEqual(model.parent_x_1.f_string, "parent_x_1")
        model = [m for m in models if m.f_string == "parent_x_1"][0]
        child_1_x = self.get_related_x_to_many(model, "child_1_x")
        self.assertEqual(len(child_1_x), 1)
        self.assertEqual(child_1_x[0].f_string, "child_1_x")

        # two_1_1
        item_two_1_1 = ItemGeneralTwo(f_string="two_1_1", f_integer="101")
        item_one_1_1 = ItemGeneralOne(f_string="one_1_1", two_1_1=item_two_1_1)
        persister.persist(item_one_1_1)

        models = self.get_all_models(self.ModelGeneralOne)
        model = [m for m in models if m.f_string == "one_1_1"][0]
        self.assertEqual(model.two_1_1.f_string, "two_1_1")
        models = self.get_all_models(self.ModelGeneralTwo)
        model = [m for m in models if m.f_string == "two_1_1"][0]
        self.assertEqual(model.one_1_1.f_string, "one_1_1")

        # two_x_1
        item_two_x_1 = ItemGeneralTwo(f_string="two_x_1", f_integer="901")
        item_one_1_x = ItemGeneralOne(f_string="one_1_x", two_x_1=item_two_x_1)
        persister.persist(item_one_1_x)
        models = self.get_all_models(self.ModelGeneralOne)
        model = [m for m in models if m.f_string == "one_1_x"][0]
        self.assertEqual(model.two_x_1.f_string, "two_x_1")
        models = self.get_all_models(self.ModelGeneralTwo)
        model = [m for m in models if m.f_string == "two_x_1"][0]
        one_1_x = self.get_related_x_to_many(model, "one_1_x")
        self.assertEqual(len(one_1_x), 1)
        self.assertEqual(one_1_x[0].f_string, "one_1_x")

    def test_forward_and_reverse_relations_filters_two(self):
        # referencing child and one

        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = [{"child_1_1"}, {"child_x_x"}, {"f_integer"}]
            getters = [{"child_1_1"}, {"two_1_1"}, {"two_x_1"}, {"f_string"}]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = [
                {"one_1_1"},
                {"one_1_x"},
                {"one_x_1"},
                {"one_x_x"},
                {"f_integer"},
            ]
            getters = [{"one_1_1"}, {"one_x_1"}, {"f_integer"}]

        persister = self.persister

        # child_1_1
        item_child_1_1 = ItemGeneralOne(f_string="child_1_1", f_integer="101")
        item_parent_1_1 = ItemGeneralOne(
            f_string="parent_1_1", child_1_1=item_child_1_1
        )
        persister.persist(item_parent_1_1)
        models = self.get_all_models(self.ModelGeneralOne)
        model = [m for m in models if m.f_string == "parent_1_1"][0]
        self.assertEqual(model.child_1_1.f_string, "child_1_1")
        model = [m for m in models if m.f_string == "child_1_1"][0]
        self.assertEqual(model.parent_1_1.f_string, "parent_1_1")

        # one_1_1
        item_one_1_1 = ItemGeneralOne(f_string="one_1_1", f_integer="1101")
        item_two_1_1 = ItemGeneralTwo(f_string="two_1_1", one_1_1=item_one_1_1)
        persister.persist(item_two_1_1)
        models = self.get_all_models(self.ModelGeneralTwo)
        model = [m for m in models if m.f_string == "two_1_1"][0]
        self.assertEqual(model.one_1_1.f_string, "one_1_1")
        models = self.get_all_models(self.ModelGeneralOne)
        model = [m for m in models if m.f_string == "one_1_1"][0]
        self.assertEqual(model.two_1_1.f_string, "two_1_1")

        # one_x_1
        item_one_x_1 = ItemGeneralOne(f_string="one_x_1", f_integer="1901")
        item_two_1_x = ItemGeneralTwo(f_string="two_1_x", one_x_1=item_one_x_1)
        persister.persist(item_two_1_x)
        models = self.get_all_models(self.ModelGeneralTwo)
        model = [m for m in models if m.f_string == "two_1_x"][0]
        self.assertEqual(model.one_x_1.f_string, "one_x_1")
        models = self.get_all_models(self.ModelGeneralOne)
        model = [m for m in models if m.f_string == "one_x_1"][0]
        two_1_x = self.get_related_x_to_many(model, "two_1_x")
        self.assertEqual(len(two_1_x), 1)
        self.assertEqual(two_1_x[0].f_string, "two_1_x")

    def test_forward_and_reverse_relations_persist(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo

        persister = self.persister

        # --- from item_one ---
        item_one = ItemGeneralOne()
        item_one["two_1_1"] = ItemGeneralTwo(f_integer="101")
        item_one["two_x_1"] = ItemGeneralTwo(f_integer="901")
        item_one["two_x_x"].gen(f_integer="909")

        persister.persist(item_one)
        models = self.get_all_models(self.ModelGeneralOne)

        self.assertEqual(len(models), 1)
        model = models[0]
        self.assertEqual(model.two_1_1.f_integer, 101)
        self.assertEqual(model.two_x_1.f_integer, 901)
        two_x_x = self.get_related_x_to_many(model, "two_x_x")
        self.assertEqual(len(two_x_x), 1)
        self.assertEqual(two_x_x[0].f_integer, 909)

        # --- from item two ---
        item_two = ItemGeneralTwo(f_string="second item")
        item_two["one_1_1"] = ItemGeneralOne(f_integer="2101")
        item_two["one_x_1"] = ItemGeneralOne(f_integer="2901")
        item_two["one_x_x"].gen(f_integer="2909")

        persister.persist(item_two)
        models = self.get_all_models(self.ModelGeneralTwo)

        # self.assertEqual(len(models), 4)  # 3 before and 1 now
        model = [m for m in models if m.f_string == "second item"][0]
        self.assertEqual(model.one_1_1.f_integer, 2101)
        self.assertEqual(model.one_x_1.f_integer, 2901)
        one_x_x = self.get_related_x_to_many(model, "one_x_x")
        self.assertEqual(len(one_x_x), 1)
        self.assertEqual(one_x_x[0].f_integer, 2909)

        # --- from self to self ---
        item_one = ItemGeneralOne(f_string="third item")
        # parent
        item_one["parent_1_1"] = ItemGeneralOne(f_integer="3101")
        item_one["parent_x_1"] = ItemGeneralOne(f_integer="3901")
        item_one["parent_x_x"].gen(f_integer="3909")
        # child
        item_one["child_1_1"] = ItemGeneralOne(f_integer="4101")
        item_one["child_1_x"].gen(f_integer="4109")
        item_one["child_x_x"].gen(f_integer="4909")

        persister.persist(item_one)
        models = self.get_all_models(self.ModelGeneralOne)
        model = [m for m in models if m.f_string == "third item"][0]

        # parent
        self.assertEqual(model.parent_1_1.f_integer, 3101)
        self.assertEqual(model.parent_x_1.f_integer, 3901)
        parent_x_x = self.get_related_x_to_many(model, "parent_x_x")
        self.assertEqual(len(parent_x_x), 1)
        self.assertEqual(parent_x_x[0].f_integer, 3909)

        # child
        self.assertEqual(model.child_1_1.f_integer, 4101)
        child_1_x = self.get_related_x_to_many(model, "child_1_x")
        self.assertEqual(len(child_1_x), 1)
        self.assertEqual(child_1_x[0].f_integer, 4109)
        child_x_x = self.get_related_x_to_many(model, "child_x_x")
        self.assertEqual(len(child_x_x), 1)
        self.assertEqual(child_x_x[0].f_integer, 4909)

    def test_persist_bulk_item(self):
        class ItemGeneralOne(Item):
            allow_merge_items = True
            model_cls = self.ModelGeneralOne
            creators = [{"f_integer"}]
            getters = [{"f_integer"}, {"f_float"}]

        class ItemGeneralTwo(Item):
            allow_merge_items = True
            model_cls = self.ModelGeneralTwo
            creators = [{"f_integer"}]
            getters = [{"f_integer"}, {"f_float"}]

        persister = self.persister

        # generating 10 simple items
        bulk_one = ItemGeneralOne.Bulk()
        for i in range(10):
            bulk_one.gen(id=i, f_integer=i + 100, f_float=i + 1000)

        previous_bulk = bulk_one.bulk[:]
        items, model_lists = persister.persist(bulk_one)
        self.assertEqual(bulk_one.bulk, previous_bulk)

        self.assertEqual(len(items), 10)
        self.assertEqual(len(items), len(model_lists))
        models = [
            model_entries[0]
            for model_entries in model_lists
            if self.assertEqual(len(model_entries), 1) is None
        ]
        models.sort(key=lambda model: model.id)
        loaded_models = self.get_all_models(self.ModelGeneralOne)
        self.assertEqual(len(models), 10)
        for i, model in enumerate(models):
            self.assertIn(model, loaded_models)
            self.assertEqual(model.id, i)
            self.assertEqual(model.f_integer, i + 100)
            self.assertEqual(model.f_float, i + 1000)
            self.assertIs(model.f_boolean, None)  # for future tests

        # generating 10 items that has previous 5 and new 5 items as relations
        bulk_two = ItemGeneralTwo.Bulk()
        for i in range(10):
            item = bulk_two.gen(id=i, f_integer=i + 200, f_float=i + 2000)
            if i < 5:
                # last five item ones will have twos with IDs from 0 to 4
                # (all `i` values), item twos from 5 to 9
                item["one_x_x"].add(*bulk_one[5:])
                item["one_x_x__f_boolean"] = "true"

                if i == 1:
                    # items are from previously saved bulk
                    # item one at index zero has ID = 0,
                    # item two has ID = 1 (current `i`, current item)
                    item["one_1_1"] = items[0]
                    item["one_1_x"].add(items[0])
            else:
                for j in range(5, 10):
                    # generated items are the same for all iterations,
                    # item twos from 5 to 9 will have item ones with IDs
                    # from 600 to 604, item ones on the other side will have
                    # item twos with IDs from 5 to 9 (all `i` values)
                    item["one_x_x"].gen(
                        id=j + 600 - 5, f_integer=j + 200 - 5, f_float=j + 2000 - 5
                    )

        items, model_lists = persister.persist(bulk_two)
        self.assertEqual(len(items), 10)
        self.assertEqual(len(items), len(model_lists))
        models = [
            model_entries[0]
            for model_entries in model_lists
            if self.assertEqual(len(model_entries), 1) is None
        ]

        # checking item twos ---------------------------------------------------

        # 5 items were not changed, 5 items were updated, 5 items were created
        all_models = self.get_all_models(self.ModelGeneralOne)
        all_models.sort(key=lambda model: model.id)
        self.assertEqual(len(all_models), 15)

        # checking fields (using IDs for relations)
        expected_values = [
            # first 5 items that were not changed
            dict(
                id=0,
                f_integer=100,
                f_float=1000.0,
                f_boolean=None,
                two_1_1=1,
                two_x_1=1,
            ),
            dict(id=1, f_integer=101, f_float=1001.0, f_boolean=None),
            dict(id=2, f_integer=102, f_float=1002.0, f_boolean=None),
            dict(id=3, f_integer=103, f_float=1003.0, f_boolean=None),
            dict(id=4, f_integer=104, f_float=1004.0, f_boolean=None),
            # 5 items that were updated
            dict(
                id=5,
                f_integer=105,
                f_float=1005.0,
                f_boolean=True,
                two_x_x=[0, 1, 2, 3, 4],
            ),
            dict(
                id=6,
                f_integer=106,
                f_float=1006.0,
                f_boolean=True,
                two_x_x=[0, 1, 2, 3, 4],
            ),
            dict(
                id=7,
                f_integer=107,
                f_float=1007.0,
                f_boolean=True,
                two_x_x=[0, 1, 2, 3, 4],
            ),
            dict(
                id=8,
                f_integer=108,
                f_float=1008.0,
                f_boolean=True,
                two_x_x=[0, 1, 2, 3, 4],
            ),
            dict(
                id=9,
                f_integer=109,
                f_float=1009.0,
                f_boolean=True,
                two_x_x=[0, 1, 2, 3, 4],
            ),
            # 5 items that were created
            dict(
                id=600,
                f_integer=200,
                f_float=2000.0,
                f_boolean=None,
                two_x_x=[5, 6, 7, 8, 9],
            ),
            dict(
                id=601,
                f_integer=201,
                f_float=2001.0,
                f_boolean=None,
                two_x_x=[5, 6, 7, 8, 9],
            ),
            dict(
                id=602,
                f_integer=202,
                f_float=2002.0,
                f_boolean=None,
                two_x_x=[5, 6, 7, 8, 9],
            ),
            dict(
                id=603,
                f_integer=203,
                f_float=2003.0,
                f_boolean=None,
                two_x_x=[5, 6, 7, 8, 9],
            ),
            dict(
                id=604,
                f_integer=204,
                f_float=2004.0,
                f_boolean=None,
                two_x_x=[5, 6, 7, 8, 9],
            ),
        ]
        relations = {
            fname: direction
            for fname, _, direction, _ in persister.db_adapter.iter_relations(
                all_models[0].__class__
            )
        }
        for i, (expected, model) in enumerate(zip(expected_values, all_models)):
            for key, value in expected.items():
                if key not in relations:
                    model_value = getattr(model, key)
                    err_msg = "{}: {} = {}".format(i, key, model_value)
                    if key != "f_boolean":
                        self.assertEqual(model_value, value, err_msg)
                    else:
                        self.assertIs(model_value, value, err_msg)
                else:
                    if relations[key].is_x_to_many():
                        model_value = self.get_related_x_to_many(model, key)
                        ids = list(m.id for m in model_value)
                        ids.sort()
                        self.assertEqual(ids, value, "{}: {} = {}".format(i, key, ids))
                    else:
                        model_value = getattr(model, key)
                        self.assertEqual(
                            model_value.id,
                            value,
                            "{}: {} = {}".format(i, key, model_value.id),
                        )

        # checking item ones ---------------------------------------------------

        # 10 items were created, other creations or no changes were made
        # (5 items were reused, 5 items were created)
        all_models = self.get_all_models(self.ModelGeneralTwo)
        all_models.sort(key=lambda model: model.id)
        self.assertEqual(len(all_models), 10)

        expected_values = [
            dict(id=0, f_integer=200, f_float=2000.0, one_x_x=[5, 6, 7, 8, 9]),
            dict(
                id=1,
                f_integer=201,
                f_float=2001.0,
                one_x_x=[5, 6, 7, 8, 9],
                one_1_1=0,
                one_1_x=[0],
            ),
            dict(id=2, f_integer=202, f_float=2002.0, one_x_x=[5, 6, 7, 8, 9]),
            dict(id=3, f_integer=203, f_float=2003.0, one_x_x=[5, 6, 7, 8, 9]),
            dict(id=4, f_integer=204, f_float=2004.0, one_x_x=[5, 6, 7, 8, 9]),
            dict(
                id=5, f_integer=205, f_float=2005.0, one_x_x=[600, 601, 602, 603, 604]
            ),
            dict(
                id=6, f_integer=206, f_float=2006.0, one_x_x=[600, 601, 602, 603, 604]
            ),
            dict(
                id=7, f_integer=207, f_float=2007.0, one_x_x=[600, 601, 602, 603, 604]
            ),
            dict(
                id=8, f_integer=208, f_float=2008.0, one_x_x=[600, 601, 602, 603, 604]
            ),
            dict(
                id=9, f_integer=209, f_float=2009.0, one_x_x=[600, 601, 602, 603, 604]
            ),
        ]
        relations = {
            fname: direction
            for fname, _, direction, _ in persister.db_adapter.iter_relations(
                all_models[0].__class__
            )
        }
        for i, (expected, model) in enumerate(zip(expected_values, all_models)):
            for key, value in expected.items():
                if key not in relations:
                    model_value = getattr(model, key)
                    err_msg = "{}: {} = {}".format(i, key, model_value)
                    self.assertEqual(model_value, value, err_msg)
                else:
                    if relations[key].is_x_to_many():
                        model_value = self.get_related_x_to_many(model, key)
                        ids = list(m.id for m in model_value)
                        ids.sort()
                        self.assertEqual(ids, value, "{}: {} = {}".format(i, key, ids))
                    else:
                        model_value = getattr(model, key)
                        self.assertEqual(
                            model_value.id,
                            value,
                            "{}: {} = {}".format(i, key, model_value.id),
                        )

    def test_persist_no_reverse_relations(self):
        class ItemAutoReverseOne(Item):
            model_cls = self.ModelAutoReverseOne

        class ItemAutoReverseTwoA(Item):
            model_cls = self.ModelAutoReverseTwoA

        class ItemAutoReverseTwoB(Item):
            model_cls = self.ModelAutoReverseTwoB

        class ItemAutoReverseThreeA(Item):
            model_cls = self.ModelAutoReverseThreeA

        class ItemAutoReverseThreeB(Item):
            model_cls = self.ModelAutoReverseThreeB

        class ItemAutoReverseFourA(Item):
            model_cls = self.ModelAutoReverseFourA

        class ItemAutoReverseFourB(Item):
            model_cls = self.ModelAutoReverseFourB

        item = ItemAutoReverseOne(f_string="one")

        item["two_b_1_1"] = ItemAutoReverseTwoB(f_string="two_b_1_1")
        item["three_b_x_1"] = ItemAutoReverseThreeB(f_string="three_b_x_1")
        item["four_b_x_x"].gen(f_string="four_b_x_x_first")
        item["four_b_x_x"].gen(f_string="four_b_x_x_second")

        persister = self.persister
        _, models_one_list = persister.persist(item)
        self.assertTrue(len(models_one_list), 1)
        models_one = models_one_list[0]
        self.assertTrue(len(models_one), 1)
        model_one_returned = models_one[0]

        # one
        models_one = self.get_all_models(self.ModelAutoReverseOne)
        self.assertEqual(len(models_one), 1)
        model_one = models_one[0]

        self.assertEqual(model_one, model_one_returned)

        # two
        models_two = self.get_all_models(self.ModelAutoReverseTwoB)
        self.assertEqual(len(models_two), 1)
        model_two = models_two[0]

        self.assertEqual(model_one.two_b_1_1, model_two)

        # three
        models_three = self.get_all_models(self.ModelAutoReverseThreeB)
        self.assertEqual(len(models_three), 1)
        model_three = models_three[0]

        self.assertEqual(model_one.three_b_x_1, model_three)

        # four
        sort_func = lambda model: model.f_string

        models_four = self.get_all_models(
            self.ModelAutoReverseFourB, sort_key=sort_func
        )
        self.assertEqual(len(models_four), 2)

        related_models_four = self.get_related_x_to_many(
            model_one, "four_b_x_x", sort_key=sort_func
        )
        self.assertEqual(len(related_models_four), 2)
        for model_four, related_model_for in zip(models_four, related_models_four):
            self.assertEqual(model_four, related_model_for)

    def test_persist_single_item_no_related(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = [{"f_integer"}]
            getters = [{"f_float"}, {"f_string", "f_text"}, {"f_date"}]

        # need this for `ItemGeneralOne` (related field)
        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo

        persister = self.persister

        # creating and updating first model ------------------------------------

        # cannot create or get
        item = ItemGeneralOne(f_string="str-one")
        items, model_lists = persister.persist(item)
        self.assertFalse(items)
        self.assertFalse(model_lists)

        # can get but not create and not in database
        item["f_text"] = "text-one"
        items, model_lists = persister.persist(item)
        self.assertFalse(items)
        self.assertFalse(model_lists)

        # can create
        item["f_integer"] = None  # `None` can create
        items, model_lists = persister.persist(item)
        self.assertEqual(len(items), 1)
        self.assertEqual(len(items), len(model_lists))
        models = model_lists[0]
        self.assertEqual(len(models), 1)
        self.assertIs(items[0], item)

        self.assertEqual(models[0].f_string, "str-one")
        self.assertEqual(models[0].f_text, "text-one")
        self.assertEqual(models[0].f_integer, None)
        self.assertEqual(models[0].f_date, None)

        # can update (`{'f_string', 'f_text'}` are the getters)

        # `f_date=None` will be used as getter later
        item["f_date"] = "2010-10-10"

        item["f_integer"] = "100"
        items, model_lists = persister.persist(item)
        self.assertEqual(len(items), 1)
        self.assertEqual(len(items), len(model_lists))
        models = model_lists[0]
        self.assertEqual(len(models), 1)
        self.assertIs(items[0], item)

        # checking that model got the same values
        self.assertEqual(models[0].f_string, "str-one")
        self.assertEqual(models[0].f_text, "text-one")
        self.assertEqual(models[0].f_integer, 100)
        self.assertEqual(models[0].f_date, datetime.date(2010, 10, 10))

        first_model = models[0]

        # creating second model ------------------------------------------------

        item = ItemGeneralOne(f_integer="200", f_string="str-one")
        items, model_lists = persister.persist(item)
        self.assertEqual(len(items), 1)
        self.assertEqual(len(items), len(model_lists))
        models = model_lists[0]
        self.assertEqual(len(models), 1)
        self.assertIs(items[0], item)

        second_model = models[0]
        self.assertNotEqual(models[0], first_model)
        self.assertEqual(models[0].f_string, "str-one")
        self.assertEqual(models[0].f_integer, 200)

        # updating first and second model --------------------------------------

        # updating second model
        item = ItemGeneralOne(f_date=None)  # `None` value can be used
        items, model_lists = persister.persist(item)
        self.assertEqual(len(models), 1)
        self.assertIs(items[0], item)
        models = model_lists[0]
        self.assertEqual(len(models), 1)
        self.assertIs(items[0], item)

        self.assertEqual(models[0], second_model)
        self.assertEqual(models[0].f_integer, 200)
        self.assertEqual(models[0].f_string, "str-one")
        self.assertEqual(models[0].f_text, None)
        self.assertIs(models[0].f_boolean, None)
        self.assertIs(models[0].f_date, None)

        # updating second model
        self.assertIs(first_model.f_boolean, None)  # will be `True` later
        item["f_string"] = "str-one"
        item["f_text"] = "text-one"
        item["f_boolean"] = True
        item["f_date"] = "2015-10-10"

        items, model_lists = persister.persist(item)
        self.assertEqual(len(items), 1)
        self.assertEqual(len(items), len(model_lists))
        models = model_lists[0]
        self.assertEqual(len(models), 1)
        self.assertIs(items[0], item)

        self.assertEqual(models[0], first_model)
        self.assertEqual(models[0].f_integer, 100)
        self.assertEqual(models[0].f_string, "str-one")
        self.assertEqual(models[0].f_text, "text-one")
        self.assertIs(models[0].f_boolean, True)
        self.assertEqual(models[0].f_date, datetime.date(2015, 10, 10))

        # return boolean to `None`
        item["f_boolean"] = None
        items, (models,) = persister.persist(item)
        self.assertEqual(models[0], first_model)
        self.assertIs(models[0].f_boolean, None)

    def test_persist_single_item_with_single_related(self):
        model_one_cls = self.ModelGeneralOne
        model_two_cls = self.ModelGeneralTwo

        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = [{"f_integer", "two_1_1"}]
            getters = [
                {"f_integer", "two_1_1"},
                {"f_float"},
                {"f_string", "f_text"},
                {"f_date"},
            ]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = [{"f_integer"}]
            getters = [{"f_integer"}]

        persister = self.persister

        # cannot create any items
        # 'two_1_1' is not present
        item_one = ItemGeneralOne(f_integer="100")
        items, model_lists = persister.persist(item_one)
        self.assertEqual(len(items), 0)
        self.assertEqual(len(items), len(model_lists))

        # 'two_1_1' cannot be created
        item_two = ItemGeneralTwo(f_float="20.20")
        items, model_lists = persister.persist(item_two)
        self.assertEqual(len(items), 0)
        self.assertEqual(len(items), len(model_lists))

        item_one["two_1_1"] = item_two
        items, model_lists = persister.persist(item_one)
        self.assertEqual(len(items), 0)
        self.assertEqual(len(items), len(model_lists))

        self.assertEqual(len(self.get_all_models(model_one_cls)), 0)
        self.assertEqual(len(self.get_all_models(model_two_cls)), 0)

        # 'two_1_1' can be created
        item_two["f_integer"] = "200"
        del item_one["f_integer"]  # won't be persisted
        items, model_lists = persister.persist(item_one)
        self.assertEqual(len(items), 0)
        self.assertEqual(len(items), len(model_lists))

        self.assertEqual(len(self.get_all_models(model_one_cls)), 0)
        # but `item_two` was persisted
        models_two = self.get_all_models(model_two_cls)
        self.assertEqual(len(models_two), 1)
        self.assertEqual(models_two[0].f_integer, 200)
        self.assertEqual(models_two[0].f_float, 20.2)

        # persisting item_one with updating existing model
        self.assertIs(models_two[0].f_boolean, None)
        item_one["f_integer"] = "100"
        item_two["f_boolean"] = True

        items, model_lists = persister.persist(item_one)
        self.assertEqual(len(items), 1)
        self.assertEqual(len(items), len(model_lists))
        self.assertEqual(len(model_lists), 1)
        models = model_lists[0]
        self.assertEqual(len(models), 1)
        self.assertIs(items[0], item_one)

        self.assertEqual(models[0].f_integer, 100)
        self.assertIsNotNone(models[0].two_1_1)
        self.assertIsNotNone(models[0].two_1_1.f_integer, 200)
        self.assertIsNotNone(models[0].two_1_1.f_float, 20.2)
        self.assertIs(models[0].two_1_1.f_boolean, True)

        self.assertEqual(len(self.get_all_models(model_one_cls)), 1)
        self.assertEqual(len(self.get_all_models(model_two_cls)), 1)

        # persisting using related item as filter
        del item_two["one_1_1"]  # old item
        item_one = ItemGeneralOne(f_integer="100", two_1_1=item_two)
        item_one["f_boolean"] = False

        items, model_lists = persister.persist(item_one)

        self.assertEqual(len(items), 1)
        self.assertEqual(len(items), len(model_lists))
        self.assertEqual(len(model_lists), 1)
        models = model_lists[0]
        self.assertEqual(len(models), 1)
        self.assertIs(items[0], item_one)
        self.assertIs(models[0].f_boolean, False)

        self.assertEqual(len(self.get_all_models(model_one_cls)), 1)
        self.assertEqual(len(self.get_all_models(model_two_cls)), 1)

    def test_persist_single_item_with_bulk_related(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = [{"id"}]
            getters = [{"id"}]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = [{"id"}]
            getters = [{"id"}]

        persister = self.persister

        item_one = ItemGeneralOne(id="5", f_float="50.50")
        item_one(
            two_1_1=ItemGeneralTwo(id="10", f_string="1-to-1"),
            two_x_1=ItemGeneralTwo(id="20", f_string="X-to-1"),
        )
        bulk = item_one["two_1_x"]
        bulk.gen(id="30", f_string="1-to-x first")
        bulk.gen(id="40", f_string="1-to-x second")
        bulk = item_one["two_x_x"]
        bulk.gen(id="50", f_string="x-to-x first")
        bulk.gen(id="60", f_string="x-to-x second")

        items, model_lists = persister.persist(item_one)

        self.assertEqual(len(items), 1)
        self.assertEqual(len(items), len(model_lists))
        models = model_lists[0]
        self.assertEqual(len(models), 1)
        self.assertIs(items[0], item_one)

        # checking model data
        model = models[0]
        self.assertEqual(model.id, 5)
        self.assertEqual(model.f_float, 50.5)

        self.assertIsNotNone(model.two_1_1)
        self.assertEqual(model.two_1_1.id, 10)
        self.assertEqual(model.two_1_1.f_string, "1-to-1")

        two_1_x = self.get_related_x_to_many(model, "two_1_x")
        two_1_x.sort(key=lambda model: model.id)
        self.assertEqual(len(two_1_x), 2)
        self.assertEqual(two_1_x[0].id, 30)
        self.assertEqual(two_1_x[0].f_string, "1-to-x first")
        self.assertEqual(two_1_x[1].id, 40)
        self.assertEqual(two_1_x[1].f_string, "1-to-x second")

        two_x_x = self.get_related_x_to_many(model, "two_x_x")
        two_x_x.sort(key=lambda model: model.id)
        self.assertEqual(len(two_x_x), 2)
        self.assertEqual(two_x_x[0].id, 50)
        self.assertEqual(two_x_x[0].f_string, "x-to-x first")
        self.assertEqual(two_x_x[1].id, 60)
        self.assertEqual(two_x_x[1].f_string, "x-to-x second")

    def test_persist_single_item_with_x_to_many_related(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = [{"f_string"}]
            getters = [{"two_1_x"}, {"f_string"}]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = [{"f_string"}]
            getters = [{"f_string"}]

        persister = self.persister

        # creating first set of items
        item_one_bulk = ItemGeneralOne.Bulk()
        for (item_name, known_names) in [
            [
                "one",
                [
                    "first",
                    "one",
                ],
            ],
            ["two", ["second", "two"]],
        ]:
            item_one = item_one_bulk.gen(f_string=item_name)
            for known_name in known_names:
                item_one["two_1_x"].gen(f_string=known_name)

        persister.persist(item_one_bulk)

        # creating second set of items
        # (`item_one` instance with name "one" must match
        #  item with name "one_more" through "first" known name)
        item_one_bulk = ItemGeneralOne.Bulk()
        for (item_name, known_names) in [
            [
                "one_more",
                [
                    "first",
                    "one_more",
                ],
            ],
            ["three", ["third", "three"]],
        ]:
            item_one = item_one_bulk.gen(f_string=item_name)
            for known_name in known_names:
                item_one["two_1_x"].gen(f_string=known_name)

        persister.persist(item_one_bulk)

        # updating two models (testing mathing x-to-many)
        item_one_bulk = ItemGeneralOne.Bulk()
        for (item_name, known_names) in [
            [
                "two_more",
                [
                    "second",
                    "two",
                    "two_more",
                ],
            ],
            ["three_more", ["third", "three", "three_more"]],
        ]:
            item_one = item_one_bulk.gen(f_string=item_name)
            for known_name in known_names:
                item_one["two_1_x"].gen(f_string=known_name)

        persister.persist(item_one_bulk)

        # checking the models
        sort_func = lambda model: model.f_string
        models_one = self.get_all_models(self.ModelGeneralOne, sort_key=sort_func)
        self.assertEqual(len(models_one), 3)

        expected = [
            ["one_more", ["first", "one", "one_more"]],
            ["two_more", ["second", "two", "two_more"]],
            ["three_more", ["third", "three", "three_more"]],
        ]
        expected.sort(key=lambda entry: entry[0])

        for i, (expected_name, expected_known_names) in enumerate(expected):

            model_one = models_one[i]
            self.assertEqual(model_one.f_string, expected_name)

            models_two = self.get_related_x_to_many(model_one, "two_1_x", sort_func)
            actual_known_names = [model_two.f_string for model_two in models_two]
            known_names.sort()
            self.assertEqual(actual_known_names, expected_known_names)

    def test_persist_with_scope(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = [{"f_integer"}]
            getters = [{"f_integer"}]

        # need this for `ItemGeneralOne` (related field)
        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = [{"f_integer"}]
            getters = [{"f_integer"}]

        scope = Scope(
            {
                ItemGeneralOne: {
                    "creators": ["f_integer", "f_float"],
                    "getters": ["f_integer", "f_float"],
                }
            },
            collection_id="test_persist_with_scope",
        )

        self.assertNotIsInstance(scope[ItemGeneralOne], ItemGeneralOne)

        persister = self.persister

        # not scoped
        item_one = ItemGeneralOne(f_integer="100", f_float="200")
        item_one_dump = persister.dumps(item_one)
        item_one_load = persister.loads(item_one_dump)
        self.assertIsInstance(item_one_load, ItemGeneralOne)

        item_one_scoped = scope[ItemGeneralOne](f_integer="300", f_float="400")
        item_one_scoped_dump = persister.dumps(item_one_scoped)
        item_one_scoped_load = persister.loads(item_one_scoped_dump)
        self.assertIsInstance(item_one_scoped_load, scope[ItemGeneralOne])

    def test_one_to_many_creator(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = ["two_1_x"]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = ["f_integer"]

        persister = self.persister

        # failed creation
        item_1 = ItemGeneralOne()
        item_1["two_1_x"]  # access creates

        items, model_lists = persister.persist(item_1)
        self.assertEqual(len(items), 0)
        self.assertEqual(len(model_lists), 0)

        # successful creation
        item_1["two_1_x"].gen(f_integer=10)
        items, model_lists = persister.persist(item_1)
        self.assertEqual(len(items), 1)
        self.assertEqual(len(model_lists), 1)
        self.assertEqual(len(model_lists[0]), 1)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 1)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 1)

        model = model_lists[0][0]
        two_1_x = self.get_related_x_to_many(model, "two_1_x")
        self.assertEqual(len(two_1_x), 1)
        self.assertEqual(two_1_x[0].f_integer, 10)

    def test_many_to_many_creator(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = ["two_x_x"]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = ["f_integer"]

        persister = self.persister

        # failed creation
        item_1 = ItemGeneralOne()
        item_1["two_x_x"]  # access creates

        items, model_lists = persister.persist(item_1)
        self.assertEqual(len(items), 0)
        self.assertEqual(len(model_lists), 0)

        # successful creation
        item_1["two_x_x"].gen(f_integer=10)
        items, model_lists = persister.persist(item_1)
        self.assertEqual(len(items), 1)
        self.assertEqual(len(model_lists), 1)
        self.assertEqual(len(model_lists[0]), 1)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 1)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 1)

        model = model_lists[0][0]
        two_x_x = self.get_related_x_to_many(model, "two_x_x")
        self.assertEqual(len(two_x_x), 1)
        self.assertEqual(two_x_x[0].f_integer, 10)

    def test_x_to_many_getters(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = ["f_integer"]
            getters = ["f_string"]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = ["f_integer"]
            getters = ["one_x_x"]

        persister = self.persister

        item_one = ItemGeneralOne(f_integer="1", f_string="str-1")
        item_two = ItemGeneralTwo(f_integer="2", one_x_x=[item_one])

        persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 1)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 1)

        item_one = ItemGeneralOne(f_string="str-1")
        item_two = ItemGeneralTwo(f_string="str-2", one_x_x=[item_one])

        persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 1)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 1)

        model_one = self.get_all_models(self.ModelGeneralOne)[0]
        model_two = self.get_all_models(self.ModelGeneralTwo)[0]

        self.assertEqual(model_one.f_integer, 1)
        self.assertEqual(model_one.f_string, "str-1")
        related_two = self.get_related_x_to_many(model_one, "two_x_x")
        self.assertEqual(len(related_two), 1)
        self.assertEqual(model_two, related_two[0])

        self.assertEqual(model_two.f_integer, 2)
        self.assertEqual(model_two.f_string, "str-2")
        related_one = self.get_related_x_to_many(model_two, "one_x_x")
        self.assertEqual(len(related_one), 1)
        self.assertEqual(model_one, related_one[0])

    def test_null_getters_and_creators(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = [{"f_integer", "f_float", "f_string", "f_text"}]
            getters = [{"f_integer", "f_string"}]
            nullables = ["f_string"]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo

        persister = self.persister
        get_key = lambda model: (
            model.f_string if model.f_string else "-",
            model.f_float,
            model.f_integer,
            model.f_text,
        )

        bulk_item = ItemGeneralOne.Bulk()
        for i in range(3):
            item = bulk_item.gen(
                f_integer=i + 1,
                f_string="str-{}".format(i + 1) if i > 1 else None,
                f_text="text-{}".format(i + 1),
            )
            if i != 0:
                item["f_float"] = i + 1 + (i + 1) / 10
        persister.persist(bulk_item)

        model_ones = self.get_all_models(self.ModelGeneralOne, sort_key=get_key)
        model_one_keys = [get_key(model) for model in model_ones]
        expect = [
            # first item cannot be created (missing `f_float`)
            # '-' stands for `None` string (otherwise not sortable)
            ("-", 2.2, 2, "text-2"),
            ("str-3", 3.3, 3, "text-3"),
        ]
        self.assertEqual(model_one_keys, expect)

        bulk_item = ItemGeneralOne.Bulk()
        for i in range(3):
            item = bulk_item.gen(
                f_integer=i + 1,
                f_string="str-{}".format(i + 1) if i > 1 else None,
                f_text="text-{}".format(i + 1 + 100),
            )
            if i != 0:
                item["f_float"] = i + 1 + (i + 1) / 10
        persister.persist(bulk_item)

        model_ones = self.get_all_models(self.ModelGeneralOne, sort_key=get_key)
        model_one_keys = [get_key(model) for model in model_ones]
        expect = [
            ("-", 2.2, 2, "text-102"),
            ("str-3", 3.3, 3, "text-103"),
        ]
        self.assertEqual(model_one_keys, expect)

    def test_one_to_many_getter(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = ["f_integer"]
            getters = ["f_integer", "two_1_x"]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = ["f_integer"]
            getters = ["f_integer"]

        persister = self.persister

        # creating dummy records
        bulk_item_one = ItemGeneralOne.Bulk()
        bulk_item_two = ItemGeneralTwo.Bulk()
        for f_integer in range(1, 4):  # 1..3
            bulk_item_one.gen(f_integer=1000 + f_integer)
            bulk_item_two.gen(f_integer=1000 + f_integer)

        persister.persist(bulk_item_one)
        persister.persist(bulk_item_two)

        # --- one-to-many ------------------------------------------------------

        # creating item one
        item_one = ItemGeneralOne(f_integer=2001)  # new
        item_one["two_1_x"].gen(f_integer=1003)  # old
        item_one["two_1_x"].gen(f_integer=1004)  # new

        _, model_lists = persister.persist(item_one)

        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)
        self.assertEqual(len(model_lists), 1)
        self.assertEqual(len(model_lists[0]), 1)

        model = model_lists[0][0]
        two_1_x = self.get_related_x_to_many(model, "two_1_x")
        self.assertEqual(len(two_1_x), 2)
        two_1_x.sort(key=lambda m: m.f_integer)
        self.assertEqual(two_1_x[0].f_integer, 1003)
        self.assertEqual(two_1_x[1].f_integer, 1004)

        # updating item one
        item_one = ItemGeneralOne()  # old, will be found by "two_1_x"
        item_one["two_1_x"].gen(f_integer=1004)  # old
        item_one["two_1_x"].gen(f_integer=1005)  # new

        _, model_lists = persister.persist(item_one)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 5)
        self.assertEqual(len(model_lists), 1)
        self.assertEqual(len(model_lists[0]), 1)

        model = model_lists[0][0]
        self.assertEqual(model.f_integer, 2001)
        two_1_x = self.get_related_x_to_many(model, "two_1_x")
        self.assertEqual(len(two_1_x), 3)
        two_1_x.sort(key=lambda m: m.f_integer)
        self.assertEqual(two_1_x[0].f_integer, 1003)
        self.assertEqual(two_1_x[1].f_integer, 1004)
        self.assertEqual(two_1_x[2].f_integer, 1005)

        # empty bulk must be ignored
        item_one = ItemGeneralOne()
        item_one["two_1_x"]  # accessing the bulk creates it
        items, model_lists = persister.persist(item_one)
        self.assertEqual(len(items), 0)
        self.assertEqual(len(model_lists), 0)
        # nothing changed
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 5)

    def test_many_to_many_getter(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = ["f_integer"]
            getters = ["f_integer"]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = ["f_integer"]
            getters = ["f_integer", "one_x_x"]

        persister = self.persister

        # creating dummy records
        bulk_item_one = ItemGeneralOne.Bulk()
        bulk_item_two = ItemGeneralTwo.Bulk()
        for f_integer in range(1, 4):  # 1..3
            bulk_item_one.gen(f_integer=1000 + f_integer)
            bulk_item_two.gen(f_integer=1000 + f_integer)

        persister.persist(bulk_item_one)
        persister.persist(bulk_item_two)

        # --- many-to-many -----------------------------------------------------

        # creating item two
        item_two = ItemGeneralTwo(f_integer=2001)  # new
        item_two["one_x_x"].gen(f_integer=1003)  # old
        item_two["one_x_x"].gen(f_integer=1004)  # new

        _, model_lists = persister.persist(item_two)

        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)
        self.assertEqual(len(model_lists), 1)
        self.assertEqual(len(model_lists[0]), 1)

        model = model_lists[0][0]
        one_x_x = self.get_related_x_to_many(model, "one_x_x")
        self.assertEqual(len(one_x_x), 2)
        one_x_x.sort(key=lambda m: m.f_integer)
        self.assertEqual(one_x_x[0].f_integer, 1003)
        self.assertEqual(one_x_x[1].f_integer, 1004)

        # updating item one
        item_two = ItemGeneralTwo()  # old, will be found by "one_x_x"
        item_two["one_x_x"].gen(f_integer=1004)  # old
        item_two["one_x_x"].gen(f_integer=1005)  # new

        _, model_lists = persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 5)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)
        self.assertEqual(len(model_lists), 1)
        self.assertEqual(len(model_lists[0]), 1)

        model = model_lists[0][0]
        self.assertEqual(model.f_integer, 2001)
        one_x_x = self.get_related_x_to_many(model, "one_x_x")
        self.assertEqual(len(one_x_x), 3)
        one_x_x.sort(key=lambda m: m.f_integer)
        self.assertEqual(one_x_x[0].f_integer, 1003)
        self.assertEqual(one_x_x[1].f_integer, 1004)
        self.assertEqual(one_x_x[2].f_integer, 1005)

        # empty bulk must be ignored
        item_two = ItemGeneralTwo()
        item_two["one_x_x"]  # accessing the bulk creates it
        items, model_lists = persister.persist(item_two)
        self.assertEqual(len(items), 0)
        self.assertEqual(len(model_lists), 0)
        # nothing changed
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 5)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)

    def test_one_to_many_getter_mix(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = ["f_integer"]
            getters = ["f_integer"]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = ["f_integer"]
            getters = ["f_integer", {"f_float", "one_1_x"}]

        persister = self.persister

        # creating dummy records
        bulk_item_one = ItemGeneralOne.Bulk()
        bulk_item_two = ItemGeneralTwo.Bulk()
        for f_integer in range(1, 4):  # 1..3
            bulk_item_one.gen(f_integer=1000 + f_integer, f_float=1000.1 + f_integer)
            bulk_item_two.gen(f_integer=1000 + f_integer, f_float=1000.1 + f_integer)

        persister.persist(bulk_item_one)
        persister.persist(bulk_item_two)

        # --- mix with regular filter ------------------------------------------

        # creating item_two
        item_two = ItemGeneralTwo(f_integer=2001, f_float=2001.2)  # new
        item_two["one_1_x"].gen(f_integer=1003)  # old
        item_two["one_1_x"].gen(f_integer=1004)  # new

        _, model_lists = persister.persist(item_two)

        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)
        self.assertEqual(len(model_lists), 1)
        self.assertEqual(len(model_lists[0]), 1)

        model = model_lists[0][0]
        one_1_x = self.get_related_x_to_many(model, "one_1_x")
        self.assertEqual(len(one_1_x), 2)
        one_1_x.sort(key=lambda m: m.f_integer)
        self.assertEqual(one_1_x[0].f_integer, 1003)
        self.assertEqual(one_1_x[1].f_integer, 1004)

        # updating item
        # (can find by `f_float`, but item from `one_1_x` is not there)
        item_two = ItemGeneralTwo(f_float=2001.2)
        item_two["one_1_x"].gen(f_integer=1001)  # old

        _, model_lists = persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)
        self.assertEqual(len(model_lists), 0)

        # (can find by `one_1_x`, but `f_float` does not match)
        item_two = ItemGeneralTwo(f_float=2002.2)
        item_two["one_1_x"].gen(f_integer=1004)  # old

        _, model_lists = persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)
        self.assertEqual(len(model_lists), 0)

        # (empty bulk)
        item_two = ItemGeneralTwo(f_float=2002.2)
        item_two["one_1_x"]  # accessing bulk creates it

        _, model_lists = persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)

        # now update must succeed
        item_two = ItemGeneralTwo(f_float=2001.2)
        item_two["one_1_x"].gen(f_integer=1004)  # old
        item_two["one_1_x"].gen(f_integer=1005)  # new

        _, model_lists = persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 5)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)

        self.assertEqual(len(model_lists), 1)
        self.assertEqual(len(model_lists[0]), 1)
        model = model_lists[0][0]
        one_1_x = self.get_related_x_to_many(model, "one_1_x")
        self.assertEqual(len(one_1_x), 3)
        one_1_x.sort(key=lambda m: m.f_integer)
        self.assertEqual(one_1_x[0].f_integer, 1003)
        self.assertEqual(one_1_x[1].f_integer, 1004)
        self.assertEqual(one_1_x[2].f_integer, 1005)

    def test_many_to_many_getter_mix(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = ["f_integer"]
            getters = ["f_integer"]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = ["f_integer"]
            getters = ["f_integer", {"f_float", "one_x_x"}]

        persister = self.persister

        # creating dummy records
        bulk_item_one = ItemGeneralOne.Bulk()
        bulk_item_two = ItemGeneralTwo.Bulk()
        for f_integer in range(1, 4):  # 1..3
            bulk_item_one.gen(f_integer=1000 + f_integer, f_float=1000.1 + f_integer)
            bulk_item_two.gen(f_integer=1000 + f_integer, f_float=1000.1 + f_integer)

        persister.persist(bulk_item_one)
        persister.persist(bulk_item_two)

        # --- mix with regular filter ------------------------------------------

        # creating item_two
        item_two = ItemGeneralTwo(f_integer=2001, f_float=2001.2)  # new
        item_two["one_x_x"].gen(f_integer=1003)  # old
        item_two["one_x_x"].gen(f_integer=1004)  # new

        _, model_lists = persister.persist(item_two)

        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)
        self.assertEqual(len(model_lists), 1)
        self.assertEqual(len(model_lists[0]), 1)

        model = model_lists[0][0]
        one_x_x = self.get_related_x_to_many(model, "one_x_x")
        self.assertEqual(len(one_x_x), 2)
        one_x_x.sort(key=lambda m: m.f_integer)
        self.assertEqual(one_x_x[0].f_integer, 1003)
        self.assertEqual(one_x_x[1].f_integer, 1004)

        # updating item
        # (can find by `f_float`, but item from `one_x_x` is not there)
        item_two = ItemGeneralTwo(f_float=2001.2)
        item_two["one_x_x"].gen(f_integer=1001)  # old

        _, model_lists = persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)
        self.assertEqual(len(model_lists), 0)

        # (can find by `one_x_x`, but `f_float` does not match)
        item_two = ItemGeneralTwo(f_float=2002.2)
        item_two["one_x_x"].gen(f_integer=1004)  # old

        _, model_lists = persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)
        self.assertEqual(len(model_lists), 0)

        # (empty bulk)
        item_two = ItemGeneralTwo(f_float=2002.2)
        item_two["one_x_x"]  # accessing bulk creates it

        _, model_lists = persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 4)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)

        # now update must succeed
        item_two = ItemGeneralTwo(f_float=2001.2)
        item_two["one_x_x"].gen(f_integer=1004)  # old
        item_two["one_x_x"].gen(f_integer=1005)  # new

        _, model_lists = persister.persist(item_two)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralOne)), 5)
        self.assertEqual(len(self.get_all_models(self.ModelGeneralTwo)), 4)

        self.assertEqual(len(model_lists), 1)
        self.assertEqual(len(model_lists[0]), 1)
        model = model_lists[0][0]
        one_x_x = self.get_related_x_to_many(model, "one_x_x")
        self.assertEqual(len(one_x_x), 3)
        one_x_x.sort(key=lambda m: m.f_integer)
        self.assertEqual(one_x_x[0].f_integer, 1003)
        self.assertEqual(one_x_x[1].f_integer, 1004)
        self.assertEqual(one_x_x[2].f_integer, 1005)

    def test_related_overwrite_with_none(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = ["two_x_1"]
            getters = ["two_x_1"]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = ["f_integer"]
            getters = ["f_integer"]

        item_one = ItemGeneralOne(two_x_1=ItemGeneralTwo(f_integer=1))
        _, model_list = self.persister.persist(item_one)
        model_one = model_list[0][0]
        self.assertEqual(model_one.two_x_1.f_integer, 1)

        item_one = ItemGeneralOne(two_x_1=None)
        _, model_list = self.persister.persist(item_one)
        self.assertEqual(len(model_list), 1)
        model_one = model_list[0][0]
        self.assertIsNone(model_one.two_x_1)

    def test_related_none_creator_and_getter(self):
        class ItemGeneralOne(Item):
            model_cls = self.ModelGeneralOne
            creators = ["two_1_1"]
            getters = ["two_1_1"]

        class ItemGeneralTwo(Item):
            model_cls = self.ModelGeneralTwo
            creators = ["f_integer"]
            getters = ["f_integer"]

        # no value - no model created
        item_one = ItemGeneralOne()
        _, model_list = self.persister.persist(item_one)
        self.assertEqual(len(model_list), 0)

        # can use `None` as creator
        item_one = ItemGeneralOne(two_1_1=None)
        _, model_list = self.persister.persist(item_one)
        self.assertEqual(len(model_list), 1)
        model_one = model_list[0][0]
        self.assertTrue(not hasattr(model_one, "two_1_1") or model_one.two_1_1 is None)

        model_one_list = self.get_all_models(self.ModelGeneralOne)
        self.assertEqual(len(model_one_list), 1)

        # updating using `None` getter
        original_model = model_one
        item_one = ItemGeneralOne(two_1_1=None, f_integer=10)
        _, model_list = self.persister.persist(item_one)
        self.assertEqual(len(model_list), 1)
        model_one = model_list[0][0]
        self.assertEqual(original_model, model_one)
        self.assertTrue(not hasattr(model_one, "two_1_1") or model_one.two_1_1 is None)
        self.assertEqual(model_one.f_integer, 10)

        model_one_list = self.get_all_models(self.ModelGeneralOne)
        self.assertEqual(len(model_one_list), 1)
