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): if is_indexable(type(obj)): query = join_lines( 'START', get_start_clause(obj, 'rel', self.type_registry), 'DELETE rel', 'RETURN 0, count(rel)' ) else: query = join_lines( 'START {}, {}', 'MATCH n1 -[rel]-> n2', 'DELETE rel', 'RETURN 0, count(rel)' ).format( get_start_clause(obj.start, 'n1', self.type_registry), get_start_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( 'START {}', 'MATCH attr -[:DECLAREDON]-> obj', 'DELETE attr', 'MATCH obj -[rel]- ()', 'DELETE obj, rel', 'RETURN count(obj), count(rel)' ).format( get_start_clause(obj, 'obj', self.type_registry) ) invalidates_types = True else: query = join_lines( 'START {}', 'MATCH obj -[rel]- ()', 'DELETE obj, rel', 'RETURN count(obj), count(rel)' ).format( get_start_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 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 = join_lines( "START", get_start_clause(C, 'C', manager.type_registry), """ RETURN C """ ) (db_attrs,) = next(manager._execute(query_str)) properties = db_attrs.get_properties() assert 'cls_attr' not in properties
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 invalidate_type_system(self): query = join_lines( 'START', get_start_clause(self.type_system, 'ts', self.type_registry), 'SET ts.version = {new_version}' ) new_version = uuid.uuid4().hex next(self._execute(query, new_version=new_version), None)
def _type_system_version(self): query = join_lines( 'START', get_start_clause(self.type_system, 'ts', self.type_registry), 'RETURN ts.version?' ) rows = self._execute(query) (version,) = next(rows) return version
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 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 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(rel_cls.__name__.upper()) query = join_lines( 'START {idx_lookup} MATCH {rel_query}', 'RETURN related, relation' ) query = query.format( idx_lookup=get_start_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 = join_lines( "START", get_start_clause(A, 'A', manager.type_registry), """ MATCH node -[:INSTANCEOF]-> () -[:ISA*0..]-> A return node """ ) results = list(manager.query(query_str)) for col, in results: assert col.cls_attr == col.__class__.cls_attr
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 get_type_hierarchy(self, start_type_id=None): """ Returns the entire type hierarchy defined in the database if start_type_id is None, else returns from that type. Returns: A generator yielding tuples of the form ``(type_id, bases, attrs)`` where - ``type_id`` identifies the type - ``bases`` lists the type_ids of the type's bases - ``attrs`` lists the attributes defined on the type """ if start_type_id: match = """ p = ( (ts:TypeSystem {id: "TypeSystem"})-[:DEFINES]->()<- [:ISA*]-(opt)<-[:ISA*0..]-(tpe) ) WHERE opt.id = {start_id} """ query_args = {'start_id': start_type_id} else: match = """ p=( (ts:TypeSystem {id: "TypeSystem"})-[:DEFINES]->()<- [:ISA*0..]-(tpe) ) """ query_args = {} query = join_lines( 'MATCH', match, """ WITH tpe, max(length(p)) AS level OPTIONAL MATCH tpe <-[:DECLAREDON*]- attr OPTIONAL MATCH tpe -[isa:ISA]-> base WITH tpe.id AS type_id, level, tpe AS class_attrs, filter( idx_base in collect(DISTINCT [isa.base_index, base.id]) WHERE not(LAST(idx_base) is NULL) ) AS bases, collect(DISTINCT attr) AS attrs ORDER BY level RETURN type_id, bases, class_attrs, attrs """) # we can't use self.query since we don't want to convert the # class_attrs dict params = dict_to_db_values_dict(query_args) for row in self._execute(query, **params): type_id, bases, class_attrs, instance_attrs = row # the bases are sorted using their index on the IsA relationship bases = tuple(base for (_, base) in sorted(bases)) class_attrs = class_attrs._properties for internal_attr in INTERNAL_CLASS_ATTRS: class_attrs.pop(internal_attr) instance_attrs = [self._convert_value(v) for v in instance_attrs] instance_attrs = {attr.name: attr for attr in instance_attrs} attrs = class_attrs attrs.update(instance_attrs) yield (type_id, bases, attrs)
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(self, cls, **attr_filter): attr_filter = dict_to_db_values_dict(attr_filter) if not attr_filter: return None query_args = {} indexes = attr_filter.items() if issubclass(cls, (Relationship, PersistableType)): idx_name = get_index_name(cls) idx_key, idx_value = indexes[0] if issubclass(cls, Relationship): self._conn.get_or_create_index(neo4j.Relationship, idx_name) start_func = 'relationship' else: self._conn.get_or_create_index(neo4j.Node, idx_name) start_func = 'node' query = 'START nr = %s:%s(%s={idx_value}) RETURN nr' % ( start_func, idx_name, idx_key) query_args['idx_value'] = idx_value elif cls is TypeSystem: idx_name = get_index_name(TypeSystem) query = join_lines( 'START ts=node:%s(id={idx_value})' % idx_name, 'RETURN ts' ) query_args['idx_value'] = self.type_system.id else: idx_where = [] for key, value in indexes: idx_where.append('n.%s! = {%s}' % (key, key)) query_args[key] = value idx_where = ' or '.join(idx_where) idx_name = get_index_name(TypeSystem) query = join_lines( 'START root=node:%s(id={idx_value})' % idx_name, 'MATCH ', ' n -[:INSTANCEOF]-> ()', ' -[:ISA*0..]-> tpe -[:ISA*0..]-> () <-[:DEFINES]- root', 'WHERE %s' % idx_where, ' AND tpe.id = {tpe_id}', 'RETURN n', ) query_args['idx_value'] = self.type_system.id type_id = get_type_id(cls) query_args['tpe_id'] = type_id found = [node for (node,) in self._execute(query, **query_args)] if not found: return None # all the nodes returned should be the same first = found[0] for node in found: if node.id != first.id: raise UniqueConstraintError(( "Multiple nodes ({}) found for unique lookup for " "{}").format(found, cls)) obj = self._convert_value(first) return obj
def _update(self, persistable, existing, changes): registry = self.type_registry for _, index_attr, _ in registry.get_index_entries(existing): if index_attr in changes: raise NotImplementedError( "We currently don't support changing unique attributes") 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 = '' index_name = get_index_name(PersistableType) query = join_lines( 'START n=node:%s(id={type_id})' % index_name, set_clauses, 'MATCH attr -[r:DECLAREDON]-> n', where, 'DELETE attr, r', 'RETURN n', ) self._update_types(persistable) else: start_clause = get_start_clause(existing, 'n', registry) query = None if isinstance(persistable, Relationship): old_start = existing.start old_end = existing.end new_start = changes.pop('start', old_start) new_end = changes.pop('end', old_end) if old_start != new_start or old_end != new_end: start_clause = '%s, %s, %s, %s, %s' % ( start_clause, get_start_clause(old_start, 'old_start', registry), get_start_clause(old_end, 'old_end', registry), get_start_clause(new_start, 'new_start', registry), get_start_clause(new_end, 'new_end', registry) ) rel_props = registry.object_to_dict(persistable) query = join_lines( 'START %s' % start_clause, 'DELETE n', 'CREATE new_start -[r:%s {rel_props}]-> new_end' % ( rel_props['__type__'].upper() ), 'RETURN r' ) query_args = {'rel_props': rel_props} if query is None: query = join_lines( 'START %s' % start_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 if isinstance(persistable, Relationship): self._index_object(persistable, result) return result
def _get_changes(self, persistable): changes = {} existing = None obj_type = type(persistable) registry = self.type_registry if isinstance(persistable, PersistableType): # this is a class, we need to get it and it's attrs idx_name = get_index_name(PersistableType) self._conn.get_or_create_index(neo4j.Node, idx_name) type_id = get_type_id(persistable) query_args = { 'type_id': type_id } query = join_lines( 'START cls=node:%s(id={type_id})' % idx_name, 'MATCH attr -[:DECLAREDON*0..]-> cls', 'RETURN cls, collect(attr.name?)' ) # don't use self.query since we don't want to convert the py2neo # node into an object rows = self._execute(query, **query_args) cls_node, attrs = next(rows, (None, None)) if cls_node is None: # have not found the cls return None, {} existing_cls_attrs = cls_node.get_properties() 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 existing = registry.get_descriptor_by_id(type_id).cls else: existing = self.get(obj_type, **get_attr_filter(persistable, registry)) if existing is not None: existing_props = registry.object_to_dict(existing) props = registry.object_to_dict(persistable) if isinstance(persistable, Relationship): # if the relationship has endoints, also consider # whether those have changed for rel_attr in ['start', 'end']: new = getattr(persistable, rel_attr, None) if new is None: continue ex_rel_attr = getattr(existing, rel_attr) ex_rel_identifier = get_attr_filter(ex_rel_attr, registry) if new != ex_rel_identifier: props[rel_attr] = new if existing_props == props: return existing, {} changes = get_changes(old=existing_props, new=props) return existing, changes