def start_path(self): """Create matching path of query.""" # Find path to model datapath = registry.path(self.label) or [] self.matches = [] self.rels = [] self.state_matches = [] for i, pathtuple in enumerate(datapath): label, relname = pathtuple self.matches.append( (label.lower, label, '({}:{})'.format(label.lower(), label))) self.rels.append( ('r{}'.format(i), relname, '-[r{}:{}]->'.format(i, relname))) if registry.state_properties(label): self.state_matches.append(label) self.addreturn(label) self.matches.append((self.label.lower(), self.label, '({}:{})'.format(self.label.lower(), self.label))) if registry.state_properties(self.label): self.state_matches.append(self.label) self.return_labels.append(self.label) return self
def test_init(self, m_state, m_side): m_state.fetch_all.return_value = [] m_side.fetch_all.return_value = [] model = 'Environment' identity = 'someid' d = Diff(model, identity, 0, 1) self.assertEqual(d.model, model) self.assertEqual(d.identity, identity) self.assertEqual(d.t1, 0) self.assertEqual(d.t2, 1) # Assert that queries are called on increasing length paths only. l_path = 0 for call in m_side.call_args_list: path = call[0][0] self.assertTrue(len(path) >= l_path) l_path = max(l_path, len(path)) # Assert that state queries are ignored for stateless models. stateless = set() for m in registry.models: if not registry.state_properties(m): stateless.add(m) for call in m_state.call_args_list: path = call[0][0] self.assertFalse(path[-1] in stateless)
def orderby(self, prop, direction, label=None): """Add to orderby clause :param prop: Property to order by :type prop: str :param direction: Order direction (ASC or DESC) :type direction: str :param label: Label with the property. Defaults to target label. Can be target label or label in parent path :type label: str """ # Default label to target label if label is None: label = self.label # Make sure label is valid model = registry.models.get(label) if model is None: raise InvalidLabelError(label) # Make sure property is valid if prop not in registry.properties(label): raise InvalidPropertyError(label, prop) # Check if property is part of the state of the model if prop in registry.state_properties(label): varname = '{}_state'.format(label.lower()) else: varname = label.lower() self._orderby.append((varname, prop, direction))
def filter(self, prop, operator, value, label=None): """Add a filter. :param prop: Name of the property :type prop: str :param operator: Operator to use :type operator: str :param value: Value to filter :type value: str :param model: Optional label that has prop. Defaults to self.label :type model: str """ if label is None: label = self.label valid_properties = registry.properties(label) if not valid_properties: raise InvalidLabelError(label) if prop not in registry.properties(label): raise InvalidPropertyError(prop, label) if prop in registry.state_properties(label): label = '{}_state'.format(label) condition = '{}.{} {} {}'.format( label.lower(), prop, operator, '$filterval{}'.format(self.filter_count)) self.filter_wheres.append(condition) self.params['filterval{}'.format(self.filter_count)] = value self.filter_count += 1 return self
def add_column(self, model, prop, name=None): """Add a column to return. Verify model is in path and prop belongs to model. Add a column tuple. These will be used in the return clause. :param model: Name of the model :type model: str :param prop: Name of the property belonging to the model. :type prop: str :param name: Optional name of the column instead of model.prop, the column name can be custom. RETURN model.prop vs RETURN model.prop AS custom :type name: str """ datapath = [p[0] for p in (registry.path(self.label) or [])] datapath += [self.label] if model not in datapath and model != self.label: raise InvalidLabelError(model) if prop not in registry.properties(model): raise InvalidPropertyError(prop, model) key = '{}.{}'.format(model, prop) if prop in registry.state_properties(model): model = _model_state(model) if name is not None: key = name self._columns[key] = '{}.{}'.format(model.lower(), prop) return self
def __init__(self, model, identity, t1, t2): """Init the diff :param model: Type|Label of the root node :type model: str :param identity: Identity of the root node :type identity: str :param t1: First timestamp in milliseconds :type t1: int :param t2: Second timestamp in milliseconds :type t2: int """ self.model = model self.identity = identity self.t1 = t1 self.t2 = t2 self.children = {} # Get list of paths paths = registry.forest.paths_from(self.model) for p in paths: q = DiffSideQuery(p, identity, (t1, t2)) for row in q.fetch_all(): self.feed(row, 't1') q = DiffSideQuery(p, identity, (t2, t1)) for row in q.fetch_all(): self.feed(row, 't2') if registry.state_properties(p[-1]): q = DiffStateQuery(p, identity, (t1, t2)) for row in q.fetch_all(): self.feed(row, ['t1', 't2'])
def fetch(self): resp = self._fetch(str(self)) rows = [] for record in resp: row = {} for label in self.return_labels: obj = {} for key, value in record[label.lower()].items(): obj[key] = value if registry.state_properties(label): state_key = '{}_state'.format(label.lower()) for key, value in record[state_key].items(): obj[key] = value row[label] = obj rows.append(row) return rows
def prune(session, env, path, stats): """Prune the leaves at the end of the path. First prune state leaves Then prune leaves Only if the type of leaf is not shared. :param session: Neo4j driver session :type session: neo4j.v1.session.BoltSession :param env: Environment to remove :type env: EnvironmentEntity :param path: List of labels indicating path. :type path: list :param stats: Dictionary of deleted node counts. :type stats: dict :returns: Updated deleted node counts. (This is changed by reference but also returned.) :rtype: dict """ leaf = path[-1] # Do nothing if the leaf is shared. if registry.is_shared(leaf): stats[leaf] = None return stats params = {'uuid': env.uuid} # First detach and delete state nodes. if registry.state_properties(leaf): match = ('MATCH (e:Environment)-[*]->(:{})-[:HAS_STATE]->(n:{}State)' 'WHERE e.uuid = $uuid').format(leaf, leaf) deleted = delete_until_zero(session, match, params=params, limit=5000) stats['{}State'.format(leaf)] = deleted # Then detach and delete identity nodes if leaf != 'Environment': match = ('MATCH (e:Environment)-[*]->(n:{})' 'WHERE e.uuid = $uuid').format(leaf) else: # Environment is a special case. # There are no paths from environment to self match = ('MATCH (n:Environment)' 'WHERE n.uuid = $uuid') deleted = delete_until_zero(session, match, params=params, limit=5000) stats[leaf] = deleted return stats
def node_at_time(self, t): """Get a node from the database at time t. :param t: A time in milliseconds :type t: int :returns: A dictionary of properties :rtype: dict """ var_models = [] rels = [] returns = ['n'] for model, reltype in self.full_path: var_models.append((model.lower(), model)) rels.append(reltype) var_models.append(('n', self.model)) if registry.state_properties(self.model): var_models.append(('ns', registry.models[self.model].state_label)) rels.append('HAS_STATE') returns.append('ns') cipher = 'MATCH p = ({}:{})'.format(var_models[0][0], var_models[0][1]) for var_model, rel in zip(var_models[1:], rels): var, model = var_model cipher += '-[:{}]->({}:{})'.format(rel, var, model) cipher += ( ' WHERE ALL (r IN RELATIONSHIPS(p) WHERE r.from <= $t < r.to) AND' ) cipher += ( ' n.{} = $identity'.format(registry.identity_property(self.model)) ) cipher += ' RETURN ' + ','.join(returns) + ' LIMIT 1' self.params['t'] = t record = self._fetch(cipher).single() if record is None: node = {} else: node = {k: v for k, v in record['n'].items()} if 'ns' in record: for k, v in record['ns'].items(): node[k] = v return node
def node_count(): """Count the number of nodes with each label. Use this before migration and after migration to verify no unwanted nodes have been created. :returns: Mapping of label to counts. :rtype: dict """ counts = {} labels = [] driver = get_neo4j() for model_name, klass in registry.models.items(): labels.append(klass.label) if registry.state_properties(model_name): labels.append(klass.state_label) for label in labels: cipher = 'MATCH (n:{}) RETURN COUNT(*) as `total`'.format(label) with driver.session() as session: with session.begin_transaction() as tx: res = tx.run(cipher).single() counts[label] = res['total'] return counts