continue implementing as_operations. finish base, lots of todos left
This commit is contained in:
parent
8dc338d547
commit
5463c058b4
1 changed files with 132 additions and 30 deletions
|
@ -1,7 +1,8 @@
|
||||||
|
import bisect
|
||||||
import operator
|
import operator
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import Type, Any, Union
|
from typing import Type, Any, Union, Self, TypeVar, Generic
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db.models import Model, Q
|
from django.db.models import Model, Q
|
||||||
|
@ -31,6 +32,8 @@ class ChangedObject(BaseSchema):
|
||||||
|
|
||||||
|
|
||||||
class OperationDependencyObjectExists(BaseSchema):
|
class OperationDependencyObjectExists(BaseSchema):
|
||||||
|
model_config = ConfigDict(frozen=True)
|
||||||
|
|
||||||
obj: ObjectReference
|
obj: ObjectReference
|
||||||
nullable: bool
|
nullable: bool
|
||||||
|
|
||||||
|
@ -57,27 +60,27 @@ OperationDependency = Union[
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SingleOperationWithDependencies(BaseSchema):
|
class SingleOperationWithDependencies[OperationT: Type[DatabaseOperation]](BaseSchema):
|
||||||
uid: tuple
|
uid: tuple
|
||||||
operation: DatabaseOperation
|
operation: OperationT
|
||||||
dependencies: set[OperationDependency] = set()
|
dependencies: set[OperationDependency] = set()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def main_operation(self) -> DatabaseOperation:
|
def main_op(self) -> Self:
|
||||||
return self.operation
|
return self
|
||||||
|
|
||||||
|
|
||||||
class MergableOperationsWithDependencies(BaseSchema):
|
class MergableOperationsWithDependencies(BaseSchema):
|
||||||
children: list[SingleOperationWithDependencies]
|
main_op: Union[
|
||||||
|
SingleOperationWithDependencies[CreateObjectOperation],
|
||||||
|
SingleOperationWithDependencies[UpdateObjectOperation],
|
||||||
|
]
|
||||||
|
sub_ops: list[SingleOperationWithDependencies[UpdateObjectOperation]]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dependencies(self) -> set[OperationDependency]:
|
def dependencies(self) -> set[OperationDependency]:
|
||||||
return reduce(operator.or_, (c.dependencies for c in self.children), set())
|
return reduce(operator.or_, (c.dependencies for c in self.children), set())
|
||||||
|
|
||||||
@property
|
|
||||||
def main_operation(self) -> DatabaseOperation:
|
|
||||||
return self.children[0].operation
|
|
||||||
|
|
||||||
|
|
||||||
OperationWithDependencies = Union[
|
OperationWithDependencies = Union[
|
||||||
SingleOperationWithDependencies,
|
SingleOperationWithDependencies,
|
||||||
|
@ -101,6 +104,9 @@ class OperationSituation(BaseSchema):
|
||||||
# operations done so far
|
# operations done so far
|
||||||
operations: list[DatabaseOperation] = []
|
operations: list[DatabaseOperation] = []
|
||||||
|
|
||||||
|
# uids of operationswithdependencies that are included now
|
||||||
|
operation_uids: frozenset[tuple] = frozenset()
|
||||||
|
|
||||||
# remaining operations still to do
|
# remaining operations still to do
|
||||||
remaining_operations_with_dependencies: list[OperationWithDependencies] = []
|
remaining_operations_with_dependencies: list[OperationWithDependencies] = []
|
||||||
|
|
||||||
|
@ -113,6 +119,22 @@ class OperationSituation(BaseSchema):
|
||||||
# references to objects that need to be removed for in this run
|
# references to objects that need to be removed for in this run
|
||||||
obj_references: dict[ModelName, dict[ObjectID, set[FoundObjectReference]]] = {}
|
obj_references: dict[ModelName, dict[ObjectID, set[FoundObjectReference]]] = {}
|
||||||
|
|
||||||
|
def fulfils_dependency(self, dependency: OperationDependency) -> bool:
|
||||||
|
if isinstance(dependency, OperationDependencyObjectExists):
|
||||||
|
return dependency.obj.id not in self.missing_objects.get(dependency.obj.model, set())
|
||||||
|
|
||||||
|
if isinstance(dependency, OperationDependencyNoProtectedReference):
|
||||||
|
return dependency.obj.id not in self.obj_references.get(dependency.obj.model, set())
|
||||||
|
|
||||||
|
if isinstance(dependency, OperationDependencyUniqueValue):
|
||||||
|
return dependency.value not in self.values_to_clear.get(dependency.obj.model,
|
||||||
|
{}).get(dependency.field, set())
|
||||||
|
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
def fulfils_dependencies(self, dependencies: set[OperationDependency]) -> bool:
|
||||||
|
return all(self.fulfils_dependency(dependency) for dependency in dependencies)
|
||||||
|
|
||||||
|
|
||||||
class ChangedObjectCollection(BaseSchema):
|
class ChangedObjectCollection(BaseSchema):
|
||||||
"""
|
"""
|
||||||
|
@ -216,13 +238,14 @@ class ChangedObjectCollection(BaseSchema):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
initial_fields = dict()
|
initial_fields = dict()
|
||||||
obj_operations: list[OperationWithDependencies] = []
|
obj_sub_operations: list[OperationWithDependencies] = []
|
||||||
|
base_dependencies: set[OperationDependency] = {OperationDependencyObjectExists(obj=changed_obj.obj)}
|
||||||
for name, value in changed_obj.fields.items():
|
for name, value in changed_obj.fields.items():
|
||||||
if value is None:
|
if value is None:
|
||||||
initial_fields[name] = None
|
initial_fields[name] = None
|
||||||
continue
|
continue
|
||||||
field = model._meta.get_field(name)
|
field = model._meta.get_field(name)
|
||||||
dependencies = set()
|
dependencies = base_dependencies.copy()
|
||||||
# todo: prev
|
# todo: prev
|
||||||
if field.is_relation:
|
if field.is_relation:
|
||||||
dependencies.add(OperationDependencyObjectExists(obj=ObjectReference(
|
dependencies.add(OperationDependencyObjectExists(obj=ObjectReference(
|
||||||
|
@ -230,35 +253,39 @@ class ChangedObjectCollection(BaseSchema):
|
||||||
id=value,
|
id=value,
|
||||||
)))
|
)))
|
||||||
if field.unique:
|
if field.unique:
|
||||||
dependencies.add(OperationDependencyUniqueValue(obj=ObjectReference(
|
dependencies.add(OperationDependencyUniqueValue(
|
||||||
model=model._meta.model_name,
|
model=model._meta.model_name,
|
||||||
field=name,
|
field=name,
|
||||||
value=value,
|
value=value,
|
||||||
)))
|
))
|
||||||
|
|
||||||
if not dependencies:
|
if not dependencies:
|
||||||
initial_fields[name] = None
|
initial_fields[name] = None
|
||||||
continue
|
continue
|
||||||
|
|
||||||
initial_fields[name] = DummyValue
|
initial_fields[name] = DummyValue
|
||||||
obj_operations.append(SingleOperationWithDependencies(
|
obj_sub_operations.append(SingleOperationWithDependencies(
|
||||||
uid=(changed_obj.obj, f"field_{name}"),
|
uid=(changed_obj.obj, f"field_{name}"),
|
||||||
operation=UpdateObjectOperation(obj=changed_obj.obj, fields={name: value}),
|
operation=UpdateObjectOperation(obj=changed_obj.obj, fields={name: value}),
|
||||||
dependencies=dependencies
|
dependencies=dependencies
|
||||||
))
|
))
|
||||||
|
|
||||||
obj_operations.insert(0, SingleOperationWithDependencies(
|
obj_main_operation = SingleOperationWithDependencies(
|
||||||
operation=(CreateObjectOperation if changed_obj.created else UpdateObjectOperation)(
|
operation=(CreateObjectOperation if changed_obj.created else UpdateObjectOperation)(
|
||||||
uid=(changed_obj.obj, f"main"),
|
uid=(changed_obj.obj, f"main"),
|
||||||
obj=changed_obj.obj,
|
obj=changed_obj.obj,
|
||||||
fields=initial_fields,
|
fields=initial_fields,
|
||||||
)
|
),
|
||||||
))
|
dependencies=base_dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
if len(obj_operations) == 1:
|
if not obj_sub_operations:
|
||||||
operations_with_dependencies.append(obj_operations[0])
|
operations_with_dependencies.append(obj_main_operation)
|
||||||
else:
|
else:
|
||||||
operations_with_dependencies.append(MergableOperationsWithDependencies(operations=obj_operations))
|
operations_with_dependencies.append(MergableOperationsWithDependencies(
|
||||||
|
main_op=obj_main_operation,
|
||||||
|
sub_ops=obj_sub_operations,
|
||||||
|
))
|
||||||
|
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
pprint(operations_with_dependencies)
|
pprint(operations_with_dependencies)
|
||||||
|
@ -270,13 +297,6 @@ class ChangedObjectCollection(BaseSchema):
|
||||||
# categorize operations to collect data for simulation/solving and problem detection
|
# categorize operations to collect data for simulation/solving and problem detection
|
||||||
missing_objects: dict[ModelName, set[ObjectID]] = {} # objects that need to exist before
|
missing_objects: dict[ModelName, set[ObjectID]] = {} # objects that need to exist before
|
||||||
for operation in operations_with_dependencies:
|
for operation in operations_with_dependencies:
|
||||||
main_operation = operation.main_operation
|
|
||||||
if isinstance(main_operation, DeleteObjectOperation):
|
|
||||||
missing_objects.setdefault(main_operation.obj.model, set()).add(main_operation.obj.id)
|
|
||||||
|
|
||||||
if isinstance(main_operation, UpdateObjectOperation):
|
|
||||||
missing_objects.setdefault(main_operation.obj.model, set()).add(main_operation.obj.id)
|
|
||||||
|
|
||||||
for dependency in operation.dependencies:
|
for dependency in operation.dependencies:
|
||||||
if isinstance(dependency, OperationDependencyObjectExists):
|
if isinstance(dependency, OperationDependencyObjectExists):
|
||||||
missing_objects.setdefault(dependency.obj.model, set()).add(dependency.obj.id)
|
missing_objects.setdefault(dependency.obj.model, set()).add(dependency.obj.id)
|
||||||
|
@ -295,6 +315,7 @@ class ChangedObjectCollection(BaseSchema):
|
||||||
# let's find which protected references objects we want to delete have
|
# let's find which protected references objects we want to delete have
|
||||||
potential_fields: dict[ModelName, dict[FieldName, dict[ModelName, set[ObjectID]]]] = {}
|
potential_fields: dict[ModelName, dict[FieldName, dict[ModelName, set[ObjectID]]]] = {}
|
||||||
for model, ids in missing_objects.items():
|
for model, ids in missing_objects.items():
|
||||||
|
# todo: this shouldn't be using missing_objects, should it?
|
||||||
for field in apps.get_model('mapdata', model)._meta.get_fields():
|
for field in apps.get_model('mapdata', model)._meta.get_fields():
|
||||||
if isinstance(field, (ManyToOneRel, OneToOneRel)) or field.model._meta.app_label != "mapdata":
|
if isinstance(field, (ManyToOneRel, OneToOneRel)) or field.model._meta.app_label != "mapdata":
|
||||||
continue
|
continue
|
||||||
|
@ -320,6 +341,87 @@ class ChangedObjectCollection(BaseSchema):
|
||||||
on_delete=model_cls._meta.get_field(field).on_delete.__name__)
|
on_delete=model_cls._meta.get_field(field).on_delete.__name__)
|
||||||
)
|
)
|
||||||
|
|
||||||
# todo: continue here
|
# todo: do the same with valuea_to_clear
|
||||||
|
|
||||||
return DatabaseOperationCollection()
|
# situations still to deal with, sorted by number of operations
|
||||||
|
open_situations: list[OperationSituation] = [start_situation]
|
||||||
|
|
||||||
|
# situation that solves for all operations
|
||||||
|
done_situation: OperationSituation | None = None
|
||||||
|
|
||||||
|
# situations that ended prematurely, todo: sort by something?
|
||||||
|
ended_situations: list[OperationSituation] = []
|
||||||
|
|
||||||
|
# situations already encountered by set of operation uuids included, values are number of operations
|
||||||
|
best_uids: dict[frozenset[tuple], int] = {}
|
||||||
|
|
||||||
|
while open_situations and not done_situation:
|
||||||
|
situation = open_situations.pop(0)
|
||||||
|
continued = False
|
||||||
|
for i, remaining_operation in enumerate(situation.remaining_operation_with_dependencies):
|
||||||
|
# check if the main operation can be ran
|
||||||
|
if not situation.fulfils_dependencies(remaining_operation.main_op.dependencies):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# determine changes to state
|
||||||
|
new_operation = remaining_operation.main_op.operation
|
||||||
|
new_remaining_operations = []
|
||||||
|
uids_to_add: set[tuple] = set(remaining_operation.main_op.uid)
|
||||||
|
if isinstance(remaining_operation, MergableOperationsWithDependencies):
|
||||||
|
# sub_ops to be merged into this one or become pending operations
|
||||||
|
new_operation: Union[CreateObjectOperation, UpdateObjectOperation]
|
||||||
|
for sub_op in remaining_operation.sub_ops:
|
||||||
|
if situation.fulfils_dependencies(sub_op.dependencies):
|
||||||
|
new_operation.fields.update(sub_op.operation.fields)
|
||||||
|
uids_to_add.add(sub_op.uid)
|
||||||
|
else:
|
||||||
|
new_remaining_operations.append(sub_op)
|
||||||
|
|
||||||
|
# todo: placeholder for references or unique values
|
||||||
|
|
||||||
|
# construct new situation
|
||||||
|
new_situation = situation.model_copy(deep=True)
|
||||||
|
new_situation.remaining_operations_with_dependencies.pop(i)
|
||||||
|
new_situation.operations.append(new_operation)
|
||||||
|
new_situation.remaining_operations_with_dependencies.extend(new_remaining_operations)
|
||||||
|
new_situation.operation_uids = new_situation.operation_uids | uids_to_add
|
||||||
|
|
||||||
|
# even if we don't actually continue cause better paths existed, this situation is not a deadlock
|
||||||
|
continued = True
|
||||||
|
|
||||||
|
if not new_situation.remaining_operations_with_dependencies:
|
||||||
|
# nothing left to do, congratulations we did it!
|
||||||
|
done_situation = new_situation
|
||||||
|
break
|
||||||
|
|
||||||
|
if best_uids.get(new_situation.operation_uids, 1000000) <= len(new_situation.operations):
|
||||||
|
# we already reached this situation with the same or less amount of operations
|
||||||
|
continue
|
||||||
|
|
||||||
|
# todo: finish this...
|
||||||
|
|
||||||
|
if isinstance(new_operation, CreateObjectOperation):
|
||||||
|
# if an object was created it's no longer missing
|
||||||
|
new_situation.missing_objects.get(new_operation.obj.model, set()).discard(new_operation.obj.id)
|
||||||
|
|
||||||
|
if isinstance(new_operation, DeleteObjectOperation):
|
||||||
|
# if an object was created it's no longer missing
|
||||||
|
new_situation.missing_objects.get(new_operation.obj.model, set()).discard(new_operation.obj.id)
|
||||||
|
|
||||||
|
# todo: ...to this
|
||||||
|
|
||||||
|
# finally insert new situation
|
||||||
|
bisect.insort(open_situations, new_situation, key=lambda s: len(s.operations))
|
||||||
|
best_uids[new_situation.operation_uids] = len(new_situation.operations)
|
||||||
|
|
||||||
|
if not continued:
|
||||||
|
ended_situations.append(situation)
|
||||||
|
|
||||||
|
if done_situation:
|
||||||
|
return DatabaseOperationCollection(
|
||||||
|
prev=self.prev,
|
||||||
|
_operations=done_situation.operations,
|
||||||
|
)
|
||||||
|
|
||||||
|
# todo: what to do if we can't fully solve it?
|
||||||
|
raise NotImplementedError('couldnt fully solve as_operations')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue