def __init__(self, name=None, abbr=None, theories=None, schema=None): super(MaterializedViewTheory, self).__init__(name=name, abbr=abbr, theories=theories, schema=schema) # queue of events left to process self.queue = EventQueue() # data storage db_name = None db_abbr = None delta_name = None delta_abbr = None if name is not None: db_name = name + "Database" delta_name = name + "Delta" if abbr is not None: db_abbr = abbr + "DB" delta_abbr = abbr + "Dlta" self.database = Database(name=db_name, abbr=db_abbr) # rules that dictate how database changes in response to events self.delta_rules = DeltaRuleTheory(name=delta_name, abbr=delta_abbr) self.kind = MATERIALIZED_POLICY_TYPE
def __init__(self, name=None, abbr=None, theories=None, schema=None): super(MaterializedViewTheory, self).__init__( name=name, abbr=abbr, theories=theories, schema=schema) # queue of events left to process self.queue = EventQueue() # data storage db_name = None db_abbr = None delta_name = None delta_abbr = None if name is not None: db_name = name + "Database" delta_name = name + "Delta" if abbr is not None: db_abbr = abbr + "DB" delta_abbr = abbr + "Dlta" self.database = Database(name=db_name, abbr=db_abbr) # rules that dictate how database changes in response to events self.delta_rules = DeltaRuleTheory(name=delta_name, abbr=delta_abbr) self.kind = MATERIALIZED_POLICY_TYPE
class MaterializedViewTheory(TopDownTheory): """A theory that stores the table contents of views explicitly. Relies on included theories to define the contents of those tables not defined by the rules of the theory. Recursive rules are allowed. """ def __init__(self, name=None, abbr=None, theories=None, schema=None): super(MaterializedViewTheory, self).__init__( name=name, abbr=abbr, theories=theories, schema=schema) # queue of events left to process self.queue = EventQueue() # data storage db_name = None db_abbr = None delta_name = None delta_abbr = None if name is not None: db_name = name + "Database" delta_name = name + "Delta" if abbr is not None: db_abbr = abbr + "DB" delta_abbr = abbr + "Dlta" self.database = Database(name=db_name, abbr=db_abbr) # rules that dictate how database changes in response to events self.delta_rules = DeltaRuleTheory(name=delta_name, abbr=delta_abbr) self.kind = MATERIALIZED_POLICY_TYPE def set_tracer(self, tracer): if isinstance(tracer, Tracer): self.tracer = tracer self.database.tracer = tracer self.delta_rules.tracer = tracer else: self.tracer = tracer['self'] self.database.tracer = tracer['database'] self.delta_rules.tracer = tracer['delta_rules'] def get_tracer(self): return {'self': self.tracer, 'database': self.database.tracer, 'delta_rules': self.delta_rules.tracer} # External Interface # SELECT is handled by TopDownTheory def insert(self, formula): return self.update([Event(formula=formula, insert=True)]) def delete(self, formula): return self.update([Event(formula=formula, insert=False)]) def update(self, events): """Apply inserts/deletes described by EVENTS and return changes. Does not check if EVENTS would cause errors. """ for event in events: assert compile.is_datalog(event.formula), ( "Non-formula not allowed: {}".format(str(event.formula))) self.enqueue_any(event) changes = self.process_queue() return changes def update_would_cause_errors(self, events): """Return a list of PolicyException. Return a list of PolicyException if we were to apply the events EVENTS to the current policy. """ self.log(None, "update_would_cause_errors %s", iterstr(events)) errors = [] # compute new rule set for event in events: assert compile.is_datalog(event.formula), ( "update_would_cause_errors operates only on objects") self.log(None, "Updating %s", event.formula) if event.formula.is_atom(): errors.extend(compile.fact_errors( event.formula, self.theories, self.name)) else: errors.extend(compile.rule_errors( event.formula, self.theories, self.name)) return errors def explain(self, query, tablenames, find_all): """Returns a list of proofs if QUERY is true or None if else.""" assert compile.is_atom(query), "Explain requires an atom" # ignoring TABLENAMES and FIND_ALL # except that we return the proper type. proof = self.explain_aux(query, 0) if proof is None: return None else: return [proof] def policy(self): return self.delta_rules.policy() def get_arity_self(self, tablename): result = self.database.get_arity_self(tablename) if result: return result return self.delta_rules.get_arity_self(tablename) # Interface implementation def explain_aux(self, query, depth): self.log(query.table, "Explaining %s", query, depth=depth) # Bail out on negated literals. Need different # algorithm b/c we need to introduce quantifiers. if query.is_negated(): return Proof(query, []) # grab first local proof, since they're all equally good localproofs = self.database.explain(query) if localproofs is None: return None if len(localproofs) == 0: # base fact return Proof(query, []) localproof = localproofs[0] rule_instance = localproof.rule.plug(localproof.binding) subproofs = [] for lit in rule_instance.body: subproof = self.explain_aux(lit, depth + 1) if subproof is None: return None subproofs.append(subproof) return Proof(query, subproofs) def modify(self, event): """Modifies contents of theory to insert/delete FORMULA. Returns True iff the theory changed. """ self.log(None, "Materialized.modify") self.enqueue_any(event) changes = self.process_queue() self.log(event.formula.tablename(), "modify returns %s", iterstr(changes)) return changes def enqueue_any(self, event): """Enqueue event. Processing rules is a bit different than processing atoms in that they generate additional events that we want to process either before the rule is deleted or after it is inserted. PROCESS_QUEUE is similar but assumes that only the data will cause propagations (and ignores included theories). """ # Note: all included theories must define MODIFY formula = event.formula if formula.is_atom(): self.log(formula.tablename(), "compute/enq: atom %s", formula) assert not self.is_view(formula.table), ( "Cannot directly modify tables" + " computed from other tables") # self.log(formula.table, "%s: %s", text, formula) self.enqueue(event) return [] else: # rules do not need to talk to included theories because they # only generate events for views # need to eliminate self-joins here so that we fill all # the tables introduced by self-join elimination. for rule in DeltaRuleTheory.eliminate_self_joins([formula]): new_event = Event(formula=rule, insert=event.insert, target=event.target) self.enqueue(new_event) return [] def enqueue(self, event): self.log(event.tablename(), "Enqueueing: %s", event) self.queue.enqueue(event) def process_queue(self): """Data and rule propagation routine. Returns list of events that were not noops """ self.log(None, "Processing queue") history = [] while len(self.queue) > 0: event = self.queue.dequeue() self.log(event.tablename(), "Dequeued %s", event) if compile.is_regular_rule(event.formula): changes = self.delta_rules.modify(event) if len(changes) > 0: history.extend(changes) bindings = self.top_down_evaluation( event.formula.variables(), event.formula.body) self.log(event.formula.tablename(), "new bindings after top-down: %s", iterstr(bindings)) self.process_new_bindings(bindings, event.formula.head, event.insert, event.formula) else: self.propagate(event) history.extend(self.database.modify(event)) self.log(event.tablename(), "History: %s", iterstr(history)) return history def propagate(self, event): """Propagate event. Computes and enqueue events generated by EVENT and the DELTA_RULES. """ self.log(event.formula.table, "Processing event: %s", event) applicable_rules = self.delta_rules.rules_with_trigger( event.formula.table) if len(applicable_rules) == 0: self.log(event.formula.table, "No applicable delta rule") for delta_rule in applicable_rules: self.propagate_rule(event, delta_rule) def propagate_rule(self, event, delta_rule): """Propagate event and delta_rule. Compute and enqueue new events generated by EVENT and DELTA_RULE. """ self.log(event.formula.table, "Processing event %s with rule %s", event, delta_rule) # compute tuples generated by event (either for insert or delete) # print "event: {}, event.tuple: {}, # event.tuple.rawtuple(): {}".format( # str(event), str(event.tuple), str(event.tuple.raw_tuple())) # binding_list is dictionary # Save binding for delta_rule.trigger; throw away binding for event # since event is ground. binding = self.new_bi_unifier() assert compile.is_literal(delta_rule.trigger) assert compile.is_literal(event.formula) undo = self.bi_unify(delta_rule.trigger, binding, event.formula, self.new_bi_unifier(), self.name) if undo is None: return self.log(event.formula.table, "binding list for event and delta-rule trigger: %s", binding) bindings = self.top_down_evaluation( delta_rule.variables(), delta_rule.body, binding) self.log(event.formula.table, "new bindings after top-down: %s", ",".join([str(x) for x in bindings])) if delta_rule.trigger.is_negated(): insert_delete = not event.insert else: insert_delete = event.insert self.process_new_bindings(bindings, delta_rule.head, insert_delete, delta_rule.original) def process_new_bindings(self, bindings, atom, insert, original_rule): """Process new bindings. For each of BINDINGS, apply to ATOM, and enqueue it as an insert if INSERT is True and as a delete otherwise. """ # for each binding, compute generated tuple and group bindings # by the tuple they generated new_atoms = {} for binding in bindings: new_atom = atom.plug(binding) if new_atom not in new_atoms: new_atoms[new_atom] = [] new_atoms[new_atom].append(Database.Proof( binding, original_rule)) self.log(atom.table, "new tuples generated: %s", iterstr(new_atoms)) # enqueue each distinct generated tuple, recording appropriate bindings for new_atom in new_atoms: # self.log(event.table, "new_tuple %s: %s", new_tuple, # new_tuples[new_tuple]) # Only enqueue if new data. # Putting the check here is necessary to support recursion. self.enqueue(Event(formula=new_atom, proofs=new_atoms[new_atom], insert=insert)) def is_view(self, x): """Return True if the table X is defined by the theory.""" return self.delta_rules.is_view(x) def is_known(self, x): """Return True if this theory has any rule mentioning table X.""" return self.delta_rules.is_known(x) def base_tables(self): """Get base tables. Return the list of tables that are mentioned in the rules but for which there are no rules with those tables in the head. """ return self.delta_rules.base_tables() def _top_down_th(self, context, caller): return self.database._top_down_th(context, caller) def content(self, tablenames=None): return self.database.content(tablenames=tablenames) def __contains__(self, formula): # TODO(thinrichs): if formula is a rule, we need to check # self.delta_rules; if formula is an atom, we need to check # self.database, but only if the table for that atom is # not defined by rules. As it stands, for atoms, we are # conflating membership with evaluation. return (formula in self.database or formula in self.delta_rules)
class MaterializedViewTheory(TopDownTheory): """A theory that stores the table contents of views explicitly. Relies on included theories to define the contents of those tables not defined by the rules of the theory. Recursive rules are allowed. """ def __init__(self, name=None, abbr=None, theories=None, schema=None): super(MaterializedViewTheory, self).__init__(name=name, abbr=abbr, theories=theories, schema=schema) # queue of events left to process self.queue = EventQueue() # data storage db_name = None db_abbr = None delta_name = None delta_abbr = None if name is not None: db_name = name + "Database" delta_name = name + "Delta" if abbr is not None: db_abbr = abbr + "DB" delta_abbr = abbr + "Dlta" self.database = Database(name=db_name, abbr=db_abbr) # rules that dictate how database changes in response to events self.delta_rules = DeltaRuleTheory(name=delta_name, abbr=delta_abbr) self.kind = MATERIALIZED_POLICY_TYPE def set_tracer(self, tracer): if isinstance(tracer, Tracer): self.tracer = tracer self.database.tracer = tracer self.delta_rules.tracer = tracer else: self.tracer = tracer['self'] self.database.tracer = tracer['database'] self.delta_rules.tracer = tracer['delta_rules'] def get_tracer(self): return { 'self': self.tracer, 'database': self.database.tracer, 'delta_rules': self.delta_rules.tracer } # External Interface # SELECT is handled by TopDownTheory def insert(self, formula): return self.update([Event(formula=formula, insert=True)]) def delete(self, formula): return self.update([Event(formula=formula, insert=False)]) def update(self, events): """Apply inserts/deletes described by EVENTS and return changes. Does not check if EVENTS would cause errors. """ for event in events: assert compile.is_datalog( event.formula), ("Non-formula not allowed: {}".format( str(event.formula))) self.enqueue_any(event) changes = self.process_queue() return changes def update_would_cause_errors(self, events): """Return a list of PolicyException. Return a list of PolicyException if we were to apply the events EVENTS to the current policy. """ self.log(None, "update_would_cause_errors %s", iterstr(events)) errors = [] # compute new rule set for event in events: assert compile.is_datalog(event.formula), ( "update_would_cause_errors operates only on objects") self.log(None, "Updating %s", event.formula) if event.formula.is_atom(): errors.extend( compile.fact_errors(event.formula, self.theories, self.name)) else: errors.extend( compile.rule_errors(event.formula, self.theories, self.name)) return errors def explain(self, query, tablenames, find_all): """Returns a list of proofs if QUERY is true or None if else.""" assert compile.is_atom(query), "Explain requires an atom" # ignoring TABLENAMES and FIND_ALL # except that we return the proper type. proof = self.explain_aux(query, 0) if proof is None: return None else: return [proof] def policy(self): return self.delta_rules.policy() def get_arity_self(self, tablename): result = self.database.get_arity_self(tablename) if result: return result return self.delta_rules.get_arity_self(tablename) # Interface implementation def explain_aux(self, query, depth): self.log(query.table, "Explaining %s", query, depth=depth) # Bail out on negated literals. Need different # algorithm b/c we need to introduce quantifiers. if query.is_negated(): return Proof(query, []) # grab first local proof, since they're all equally good localproofs = self.database.explain(query) if localproofs is None: return None if len(localproofs) == 0: # base fact return Proof(query, []) localproof = localproofs[0] rule_instance = localproof.rule.plug(localproof.binding) subproofs = [] for lit in rule_instance.body: subproof = self.explain_aux(lit, depth + 1) if subproof is None: return None subproofs.append(subproof) return Proof(query, subproofs) def modify(self, event): """Modifies contents of theory to insert/delete FORMULA. Returns True iff the theory changed. """ self.log(None, "Materialized.modify") self.enqueue_any(event) changes = self.process_queue() self.log(event.formula.tablename(), "modify returns %s", iterstr(changes)) return changes def enqueue_any(self, event): """Enqueue event. Processing rules is a bit different than processing atoms in that they generate additional events that we want to process either before the rule is deleted or after it is inserted. PROCESS_QUEUE is similar but assumes that only the data will cause propagations (and ignores included theories). """ # Note: all included theories must define MODIFY formula = event.formula if formula.is_atom(): self.log(formula.tablename(), "compute/enq: atom %s", formula) assert not self.is_view( formula.table), ("Cannot directly modify tables" + " computed from other tables") # self.log(formula.table, "%s: %s", text, formula) self.enqueue(event) return [] else: # rules do not need to talk to included theories because they # only generate events for views # need to eliminate self-joins here so that we fill all # the tables introduced by self-join elimination. for rule in DeltaRuleTheory.eliminate_self_joins([formula]): new_event = Event(formula=rule, insert=event.insert, target=event.target) self.enqueue(new_event) return [] def enqueue(self, event): self.log(event.tablename(), "Enqueueing: %s", event) self.queue.enqueue(event) def process_queue(self): """Data and rule propagation routine. Returns list of events that were not noops """ self.log(None, "Processing queue") history = [] while len(self.queue) > 0: event = self.queue.dequeue() self.log(event.tablename(), "Dequeued %s", event) if compile.is_regular_rule(event.formula): changes = self.delta_rules.modify(event) if len(changes) > 0: history.extend(changes) bindings = self.top_down_evaluation( event.formula.variables(), event.formula.body) self.log(event.formula.tablename(), "new bindings after top-down: %s", iterstr(bindings)) self.process_new_bindings(bindings, event.formula.head, event.insert, event.formula) else: self.propagate(event) history.extend(self.database.modify(event)) self.log(event.tablename(), "History: %s", iterstr(history)) return history def propagate(self, event): """Propagate event. Computes and enqueue events generated by EVENT and the DELTA_RULES. """ self.log(event.formula.table, "Processing event: %s", event) applicable_rules = self.delta_rules.rules_with_trigger( event.formula.table) if len(applicable_rules) == 0: self.log(event.formula.table, "No applicable delta rule") for delta_rule in applicable_rules: self.propagate_rule(event, delta_rule) def propagate_rule(self, event, delta_rule): """Propagate event and delta_rule. Compute and enqueue new events generated by EVENT and DELTA_RULE. """ self.log(event.formula.table, "Processing event %s with rule %s", event, delta_rule) # compute tuples generated by event (either for insert or delete) # print "event: {}, event.tuple: {}, # event.tuple.rawtuple(): {}".format( # str(event), str(event.tuple), str(event.tuple.raw_tuple())) # binding_list is dictionary # Save binding for delta_rule.trigger; throw away binding for event # since event is ground. binding = self.new_bi_unifier() assert compile.is_literal(delta_rule.trigger) assert compile.is_literal(event.formula) undo = self.bi_unify(delta_rule.trigger, binding, event.formula, self.new_bi_unifier(), self.name) if undo is None: return self.log(event.formula.table, "binding list for event and delta-rule trigger: %s", binding) bindings = self.top_down_evaluation(delta_rule.variables(), delta_rule.body, binding) self.log(event.formula.table, "new bindings after top-down: %s", ",".join([str(x) for x in bindings])) if delta_rule.trigger.is_negated(): insert_delete = not event.insert else: insert_delete = event.insert self.process_new_bindings(bindings, delta_rule.head, insert_delete, delta_rule.original) def process_new_bindings(self, bindings, atom, insert, original_rule): """Process new bindings. For each of BINDINGS, apply to ATOM, and enqueue it as an insert if INSERT is True and as a delete otherwise. """ # for each binding, compute generated tuple and group bindings # by the tuple they generated new_atoms = {} for binding in bindings: new_atom = atom.plug(binding) if new_atom not in new_atoms: new_atoms[new_atom] = [] new_atoms[new_atom].append(Database.Proof(binding, original_rule)) self.log(atom.table, "new tuples generated: %s", iterstr(new_atoms)) # enqueue each distinct generated tuple, recording appropriate bindings for new_atom in new_atoms: # self.log(event.table, "new_tuple %s: %s", new_tuple, # new_tuples[new_tuple]) # Only enqueue if new data. # Putting the check here is necessary to support recursion. self.enqueue( Event(formula=new_atom, proofs=new_atoms[new_atom], insert=insert)) def is_view(self, x): """Return True if the table X is defined by the theory.""" return self.delta_rules.is_view(x) def is_known(self, x): """Return True if this theory has any rule mentioning table X.""" return self.delta_rules.is_known(x) def base_tables(self): """Get base tables. Return the list of tables that are mentioned in the rules but for which there are no rules with those tables in the head. """ return self.delta_rules.base_tables() def _top_down_th(self, context, caller): return self.database._top_down_th(context, caller) def content(self, tablenames=None): return self.database.content(tablenames=tablenames) def __contains__(self, formula): # TODO(thinrichs): if formula is a rule, we need to check # self.delta_rules; if formula is an atom, we need to check # self.database, but only if the table for that atom is # not defined by rules. As it stands, for atoms, we are # conflating membership with evaluation. return (formula in self.database or formula in self.delta_rules)