def test_reload_external_changes(manager, connection, static_types): Thing = static_types['Thing'] manager.save(Thing) manager.reload_types() # cache type registry # update the graph as an external manager would # (change a value and bump the typesystem version) match_clauses = ( get_match_clause(Thing, 'Thing', manager.type_registry), get_match_clause(manager.type_system, 'ts', manager.type_registry), ) query = join_lines( 'MATCH', (match_clauses, ','), 'SET ts.version = {version}', 'SET Thing.cls_attr = {cls_attr}', 'RETURN Thing' ) query_params = { 'cls_attr': 'placeholder', 'version': str(uuid.uuid4()) } cypher.execute(connection, query, query_params) # reloading types should see the difference manager.reload_types() descriptor = manager.type_registry.get_descriptor(Thing) assert "cls_attr" in descriptor.class_attributes
def test_get_match_clause_for_relationship_non_unique_endpoint(cls): a = cls() b = cls() rel = Connects(start=a, end=b) with pytest.raises(NoUniqueAttributeError) as exc: get_match_clause(rel, 'rel', type_registry) assert "doesn't have any unique attributes" in str(exc)
def delete(self, obj): """ Deletes an object from the store. Args: obj: The object to delete. Returns: A tuple: with (number of nodes removed, number of rels removed) """ invalidates_types = False if isinstance(obj, Relationship): query = join_lines( 'MATCH {}, {},', 'n1 -[rel]-> n2', 'DELETE rel', 'RETURN 0, count(rel)' ).format( get_match_clause(obj.start, 'n1', self.type_registry), get_match_clause(obj.end, 'n2', self.type_registry), ) rel_type = type(obj) if rel_type in (IsA, DeclaredOn): invalidates_types = True elif isinstance(obj, PersistableType): query = join_lines( 'MATCH {}', 'OPTIONAL MATCH attr -[:DECLAREDON]-> obj', 'DELETE attr', 'WITH obj', 'MATCH obj -[rel]- ()', 'DELETE obj, rel', 'RETURN count(obj), count(rel)' ).format( get_match_clause(obj, 'obj', self.type_registry) ) invalidates_types = True else: query = join_lines( 'MATCH {},', 'obj -[rel]- ()', 'DELETE obj, rel', 'RETURN count(obj), count(rel)' ).format( get_match_clause(obj, 'obj', self.type_registry) ) # TODO: delete node/rel from indexes res = next(self._execute(query)) if invalidates_types: self.invalidate_type_system() return res
def update_type(self, tpe, bases): """ Change the bases of the given ``tpe`` """ if not isinstance(tpe, PersistableType): raise UnsupportedTypeError("Object is not a PersistableType") if self.type_registry.is_static_type(tpe): raise CannotUpdateType("Type '{}' is defined in code and cannot" "be updated.".format(get_type_id(tpe))) descriptor = self.type_registry.get_descriptor(tpe) existing_attrs = dict_difference(descriptor.attributes, descriptor.declared_attributes) base_attrs = {} for base in bases: desc = self.type_registry.get_descriptor(base) base_attrs.update(desc.attributes) base_attrs = dict_difference(base_attrs, descriptor.declared_attributes) if existing_attrs != base_attrs: raise CannotUpdateType("Inherited attributes are not identical") match_clauses = [get_match_clause(tpe, 'type', self.type_registry)] create_clauses = [] query_args = {} for index, base in enumerate(bases): name = 'base_{}'.format(index) match = get_match_clause(base, name, self.type_registry) create = "type -[:ISA {%s_props}]-> %s" % (name, name) query_args["{}_props".format(name)] = {'base_index': index} match_clauses.append(match) create_clauses.append(create) query = join_lines( "MATCH", (match_clauses, ','), ", type -[r:ISA]-> ()", "DELETE r", "CREATE", (create_clauses, ','), "RETURN type") try: next(self._execute(query, **query_args)) self.invalidate_type_system() except StopIteration: raise CannotUpdateType("Type or bases not found in the database.") self.reload_types()
def test_class_attr_class_serialization(manager): with collector() as classes: class A(Entity): id = Uuid() cls_attr = "spam" class B(A): cls_attr = "ham" class C(B): pass manager.save_collected_classes(classes) # we want inherited attributes when we serialize assert manager.serialize(C) == { '__type__': 'PersistableType', 'id': 'C', 'cls_attr': 'ham', } # we don't want inherited attributes in the db query_str = "MATCH {} RETURN C".format( get_match_clause(C, 'C', manager.type_registry), ) (db_attrs,) = next(manager._execute(query_str)) properties = db_attrs.get_properties() assert 'cls_attr' not in properties
def has_property(manager, obj, prop): query = "MATCH {} RETURN node.{}".format( get_match_clause(obj, 'node', manager.type_registry), prop, ) properties = list(manager.query(query)) return not (properties == [(None,)])
def test_get_match_clause_for_relationship(): a = IndexableThing(indexable_attr='a') b = IndexableThing(indexable_attr='b') rel = Connects(start=a, end=b) match_clause = get_match_clause(rel, 'rel', type_registry) expected = """ (rel__start:IndexableThing {indexable_attr: "a"}), (rel__end:IndexableThing {indexable_attr: "b"}), (rel__start)-[rel:CONNECTS]->(rel__end) """ assert match_clause == dedent(expected)
def get_instance_of_relationship(manager, obj): query = """ MATCH {}, (node)-[instance_of:INSTANCEOF]->() RETURN instance_of """.format( get_match_clause(obj, 'node', manager.type_registry), ) instance_of = manager.query_single(query) return instance_of
def test_get_match_clause_mutiple_uniques(): obj = TwoUniquesThing( indexable_attr="bar", also_unique="baz" ) match_clause = get_match_clause(obj, "foo", type_registry) # order if labels and properties are undefined, so try all possibilities possible_labels = ['IndexableThing', 'TwoUniquesThing'] possible_attrs = ['indexable_attr: "bar"', 'also_unique: "baz"'] possible_clauses = set() for labels in permutations(possible_labels, 2): for attrs in permutations(possible_attrs, 2): clause = '(foo:{} {{{}}})'.format( ':'.join(labels), ', '.join(attrs) ) possible_clauses.add(clause) assert match_clause in possible_clauses
def get_related_objects(self, rel_cls, ref_cls, obj): if ref_cls is Outgoing: rel_query = '(n)-[relation:{}]->(related)' elif ref_cls is Incoming: rel_query = '(n)<-[relation:{}]-(related)' # TODO: should get the rel name from descriptor? rel_query = rel_query.format(get_neo4j_relationship_name(rel_cls)) query = join_lines( 'MATCH {idx_lookup}, {rel_query}' 'RETURN related, relation' ) query = query.format( idx_lookup=get_match_clause(obj, 'n', self.type_registry), rel_query=rel_query ) return self.query(query)
def test_class_att_overriding(manager): with collector() as classes: class A(Entity): id = Uuid() cls_attr = "spam" class B(A): cls_attr = "ham" class C(B): pass manager.save_collected_classes(classes) manager.reload_types() a = A() b = B() c = C() assert a.cls_attr == "spam" assert b.cls_attr == "ham" assert c.cls_attr == "ham" manager.save(a) manager.save(b) manager.save(c) query_str = """ MATCH {}, (node)-[:INSTANCEOF]->()-[:ISA*0..]->A RETURN node """.format( get_match_clause(A, 'A', manager.type_registry), ) results = list(manager.query(query_str)) for col, in results: assert col.cls_attr == col.__class__.cls_attr
def test_get_match_clause_bad_unique_value(): with pytest.raises(NoUniqueAttributeError): get_match_clause(IndexableThing( indexable_attr=None), 'foo', type_registry)
def test_get_match_clause_no_uniques(): with pytest.raises(NoUniqueAttributeError): get_match_clause(NotIndexable(), 'foo', type_registry)
def test_get_match_clause_for_instance(): obj = IndexableThing(indexable_attr="bar") clause = get_match_clause(obj, "foo", type_registry) assert clause == '(foo:IndexableThing {indexable_attr: "bar"})'
def test_get_match_clause_for_type(): clause = get_match_clause(IndexableThing, "foo", type_registry) assert clause == '(foo:PersistableType {id: "IndexableThing"})'
def test_get_match_clause_for_relationship_missing_endpoint(): rel = Connects() with pytest.raises(NoUniqueAttributeError) as exc: get_match_clause(rel, 'rel', type_registry) assert 'is missing a start or end node' in str(exc)
def change_instance_type(self, obj, type_id, updated_values=None): if updated_values is None: updated_values = {} type_registry = self.type_registry if type_id not in type_registry._types_in_db: raise TypeNotPersistedError(type_id) properties = self.serialize(obj, for_db=True) properties['__type__'] = type_id properties.update(updated_values) # get rid of any attributes not supported by the new type properties = self.serialize(self.deserialize(properties), for_db=True) old_type = type(obj) new_type = type_registry.get_class_by_id(type_id) rel_props = type_registry.object_to_dict(InstanceOf(), for_db=True) old_labels = set(type_registry.get_labels_for_type(old_type)) new_labels = set(type_registry.get_labels_for_type(new_type)) removed_labels = old_labels - new_labels added_labels = new_labels - old_labels if removed_labels: remove_labels_statement = 'REMOVE obj:' + ':'.join(removed_labels) else: remove_labels_statement = '' if added_labels: add_labels_statement = 'SET obj :' + ':'.join(added_labels) else: add_labels_statement = '' match_clauses = ( get_match_clause(obj, 'obj', type_registry), get_match_clause(new_type, 'type', type_registry) ) query = join_lines( 'MATCH', (match_clauses, ','), ', (obj)-[old_rel:INSTANCEOF]->()', 'DELETE old_rel', 'CREATE (obj)-[new_rel:INSTANCEOF {rel_props}]->(type)', 'SET obj={properties}', remove_labels_statement, add_labels_statement, 'RETURN obj', ) new_obj = self.query_single( query, properties=properties, rel_props=rel_props) if new_obj is None: raise NoResultFound( "{} not found in db".format(repr(obj)) ) set_store_for_object(new_obj, self) return new_obj
def _update(self, persistable, existing, changes): registry = self.type_registry set_clauses = ', '.join([ 'n.%s={%s}' % (key, key) for key, value in changes.items() if not isinstance(value, dict) ]) if set_clauses: set_clauses = 'SET %s' % set_clauses else: set_clauses = '' if isinstance(persistable, type): query_args = {'type_id': get_type_id(persistable)} class_attr_changes = {k: v for k, v in changes.items() if k != 'attributes'} query_args.update(class_attr_changes) where = [] descr = registry.get_descriptor(persistable) for attr_name in descr.declared_attributes.keys(): where.append('attr.name = {attr_%s}' % attr_name) query_args['attr_%s' % attr_name] = attr_name if where: where = ' OR '.join(where) where = 'WHERE not(%s)' % where else: where = '' query = join_lines( 'MATCH (n:PersistableType)', 'WHERE n.id = {type_id}', set_clauses, 'WITH n', 'MATCH attr -[r:DECLAREDON]-> n', where, 'DELETE attr, r', 'RETURN n', ) self._update_types(persistable) else: match_clause = get_match_clause(existing, 'n', registry) query = join_lines( 'MATCH %s' % match_clause, set_clauses, 'RETURN n' ) query_args = changes try: (result,) = next(self._execute(query, **query_args)) except StopIteration: # this can happen, if no attributes where changed on a type result = persistable return result
def _get_changes(self, persistable): changes = {} existing = None registry = self.type_registry if isinstance(persistable, PersistableType): if issubclass(persistable, Relationship): # not stored in the db; must be static return None, {} query = """ MATCH {} OPTIONAL MATCH (attr)-[:DECLAREDON*0..]->(cls) RETURN cls, collect(attr.name) """.format(get_match_clause(persistable, 'cls', registry)) # don't use self.query since we don't want to convert the py2neo # node into an object rows = self._execute(query) cls_node, attrs = next(rows, (None, None)) if cls_node is None: # have not found the cls return None, {} existing_cls_attrs = cls_node._properties # Make sure we get a clean view of current data. registry.refresh_type(persistable) new_cls_attrs = registry.object_to_dict(persistable) # If any existing keys in "new" are missing in "old", add `None`s. # Unlike instance attributes, we just need to remove the properties # from the node, which we can achieve by setting the values to None for key in set(existing_cls_attrs) - set(new_cls_attrs): new_cls_attrs[key] = None changes = get_changes(old=existing_cls_attrs, new=new_cls_attrs) attrs = set(attrs) modified_attrs = {} descr = registry.get_descriptor(persistable) for name, attr in descr.declared_attributes.items(): if name not in attrs: modified_attrs[name] = attr del_attrs = set(attrs) for name in Descriptor(persistable).attributes.keys(): del_attrs.discard(name) for name in del_attrs: modified_attrs[name] = None if modified_attrs: changes['attributes'] = modified_attrs # we want to return the existing class type_id = get_type_id(persistable) existing = registry.get_descriptor_by_id(type_id).cls else: try: query = 'MATCH {} RETURN obj'.format( get_match_clause(persistable, 'obj', registry) ) except NoUniqueAttributeError: existing = None else: existing = self.query_single(query) if existing is not None: existing_props = registry.object_to_dict(existing) props = registry.object_to_dict(persistable) if existing_props == props: return existing, {} changes = get_changes(old=existing_props, new=props) return existing, changes