Exemple #1
0
 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
Exemple #2
0
 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
Exemple #3
0
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)
Exemple #4
0
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)