def __init__(self, name=None, abbr=None, schema=None, theories=None): super(NonrecursiveRuleTheory, self).__init__(name=name, abbr=abbr, theories=theories, schema=schema) # dictionary from table name to list of rules with that table in head self.rules = RuleSet() self.kind = NONRECURSIVE_POLICY_TYPE
def __init__(self, name=None, abbr=None, theories=None): super(DeltaRuleTheory, self).__init__(name=name, abbr=abbr, theories=theories) # dictionary from table name to list of rules with that table as # trigger self.rules = RuleSet() # dictionary from delta_rule to the rule from which it was derived self.originals = set() # dictionary from table name to number of rules with that table in # head self.views = {} # all tables self.all_tables = {} self.kind = DELTA_POLICY_TYPE
class DeltaRuleTheory(Theory): """A collection of DeltaRules. Not useful by itself as a policy.""" def __init__(self, name=None, abbr=None, theories=None): super(DeltaRuleTheory, self).__init__(name=name, abbr=abbr, theories=theories) # dictionary from table name to list of rules with that table as # trigger self.rules = RuleSet() # dictionary from delta_rule to the rule from which it was derived self.originals = set() # dictionary from table name to number of rules with that table in # head self.views = {} # all tables self.all_tables = {} self.kind = DELTA_POLICY_TYPE def modify(self, event): """Insert/delete the compile.Rule RULE into the theory. Return list of changes (either the empty list or a list including just RULE). """ self.log(None, "DeltaRuleTheory.modify %s", event.formula) self.log(None, "originals: %s", iterstr(self.originals)) if event.insert: if self.insert(event.formula): return [event] else: if self.delete(event.formula): return [event] return [] def insert(self, rule): """Insert a compile.Rule into the theory. Return True iff the theory changed. """ assert compile.is_regular_rule(rule), ( "DeltaRuleTheory only takes rules") self.log(rule.tablename(), "Insert: %s", rule) if rule in self.originals: self.log(None, iterstr(self.originals)) return False self.log(rule.tablename(), "Insert 2: %s", rule) for delta in self.compute_delta_rules([rule]): self.insert_delta(delta) self.originals.add(rule) return True def insert_delta(self, delta): """Insert a delta rule.""" self.log(None, "Inserting delta rule %s", delta) # views (tables occurring in head) if delta.head.table in self.views: self.views[delta.head.table] += 1 else: self.views[delta.head.table] = 1 # tables for table in delta.tablenames(): if table in self.all_tables: self.all_tables[table] += 1 else: self.all_tables[table] = 1 # contents # TODO(thinrichs): eliminate dups, maybe including # case where bodies are reorderings of each other self.rules.add_rule(delta.trigger.table, delta) def delete(self, rule): """Delete a compile.Rule from theory. Assumes that COMPUTE_DELTA_RULES is deterministic. Returns True iff the theory changed. """ self.log(rule.tablename(), "Delete: %s", rule) if rule not in self.originals: return False for delta in self.compute_delta_rules([rule]): self.delete_delta(delta) self.originals.remove(rule) return True def delete_delta(self, delta): """Delete the DeltaRule DELTA from the theory.""" # views if delta.head.table in self.views: self.views[delta.head.table] -= 1 if self.views[delta.head.table] == 0: del self.views[delta.head.table] # tables for table in delta.tablenames(): if table in self.all_tables: self.all_tables[table] -= 1 if self.all_tables[table] == 0: del self.all_tables[table] # contents self.rules.discard_rule(delta.trigger.table, delta) def policy(self): return self.originals def get_arity_self(self, tablename): for p in self.originals: if p.head.table == tablename: return len(p.head.arguments) return None def __contains__(self, formula): return formula in self.originals def __str__(self): return str(self.rules) def rules_with_trigger(self, table): """Return the list of DeltaRules that trigger on the given TABLE.""" if table in self.rules: return self.rules.get_rules(table) else: return [] def is_view(self, x): return x in self.views def is_known(self, x): return x in self.all_tables def base_tables(self): base = [] for table in self.all_tables: if table not in self.views: base.append(table) return base @classmethod def eliminate_self_joins(cls, formulas): """Remove self joins. Return new list of formulas that is equivalent to the list of formulas FORMULAS except that there are no self-joins. """ def new_table_name(name, arity, index): return "___{}_{}_{}".format(name, arity, index) def n_variables(n): vars = [] for i in xrange(0, n): vars.append("x" + str(i)) return vars # dict from (table name, arity) tuple to # max num of occurrences of self-joins in any rule global_self_joins = {} # remove self-joins from rules results = [] for rule in formulas: if rule.is_atom(): results.append(rule) continue LOG.debug("eliminating self joins from %s", rule) occurrences = {} # for just this rule for atom in rule.body: table = atom.tablename() arity = len(atom.arguments) tablearity = (table, arity) if tablearity not in occurrences: occurrences[tablearity] = 1 else: # change name of atom atom.table = new_table_name(table, arity, occurrences[tablearity]) # update our counters occurrences[tablearity] += 1 if tablearity not in global_self_joins: global_self_joins[tablearity] = 1 else: global_self_joins[tablearity] = (max( occurrences[tablearity] - 1, global_self_joins[tablearity])) results.append(rule) LOG.debug("final rule: %s", rule) # add definitions for new tables for tablearity in global_self_joins: table = tablearity[0] arity = tablearity[1] for i in xrange(1, global_self_joins[tablearity] + 1): newtable = new_table_name(table, arity, i) args = [compile.Variable(var) for var in n_variables(arity)] head = compile.Literal(newtable, args) body = [compile.Literal(table, args)] results.append(compile.Rule(head, body)) LOG.debug("Adding rule %s", results[-1]) return results @classmethod def compute_delta_rules(cls, formulas): """Return list of DeltaRules computed from formulas. Assuming FORMULAS has no self-joins, return a list of DeltaRules derived from those FORMULAS. """ # Should do the following for correctness, but it needs to be # done elsewhere so that we can properly maintain the tables # that are generated. # formulas = cls.eliminate_self_joins(formulas) delta_rules = [] for rule in formulas: if rule.is_atom(): continue rule = compile.reorder_for_safety(rule) for literal in rule.body: if builtin_registry.is_builtin(literal.table, len(literal.arguments)): continue newbody = [lit for lit in rule.body if lit is not literal] delta_rules.append(DeltaRule(literal, rule.head, newbody, rule)) return delta_rules
class NonrecursiveRuleTheory(TopDownTheory): """A non-recursive collection of Rules.""" def __init__(self, name=None, abbr=None, schema=None, theories=None): super(NonrecursiveRuleTheory, self).__init__(name=name, abbr=abbr, theories=theories, schema=schema) # dictionary from table name to list of rules with that table in head self.rules = RuleSet() self.kind = NONRECURSIVE_POLICY_TYPE # External Interface # SELECT implemented by TopDownTheory def insert(self, rule): changes = self.update([Event(formula=rule, insert=True)]) return [event.formula for event in changes] def delete(self, rule): changes = self.update([Event(formula=rule, insert=False)]) return [event.formula for event in changes] def update(self, events): """Apply EVENTS. And return the list of EVENTS that actually changed the theory. Each event is the insert or delete of a policy statement. """ changes = [] self.log(None, "Update %s", iterstr(events)) try: for event in events: formula = compile.reorder_for_safety(event.formula) if event.insert: if self.insert_actual(formula): changes.append(event) else: if self.delete_actual(formula): changes.append(event) except Exception as e: LOG.exception("runtime caught an exception") raise e return changes def update_would_cause_errors(self, events): """Return a list of compile.CongressException. Return a list of compile.CongressException if we were to apply the insert/deletes of policy statements dictated by EVENTS to the current policy. """ self.log(None, "update_would_cause_errors %s", iterstr(events)) errors = [] for event in events: if not compile.is_datalog(event.formula): errors.append( compile.CongressException("Non-formula found: {}".format( str(event.formula)))) else: 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)) # Would also check that rules are non-recursive, but that # is currently being handled by Runtime. The current implementation # disallows recursion in all theories. return errors def define(self, rules): """Empties and then inserts RULES.""" self.empty() return self.update( [Event(formula=rule, insert=True) for rule in rules]) def empty(self): """Deletes contents of theory.""" self.rules.clear() def policy(self): # eliminate all rules with empty bodies return [p for p in self.content() if len(p.body) > 0] def get_arity_self(self, tablename, theory): if tablename not in self.rules: return None rules = self.rules.get_rules(tablename) if len(rules) == 0: return None try: rule = next(rule for rule in rules if rule.head.theory == theory) except StopIteration: return None return len(rule.head.arguments) def __contains__(self, formula): return formula in self.rules # Internal Interface def insert_actual(self, rule): """Insert RULE and return True if there was a change.""" if compile.is_atom(rule): rule = compile.Rule(rule, [], rule.location) self.log(rule.head.table, "Insert: %s", rule) return self.rules.add_rule(rule.head.table, rule) def delete_actual(self, rule): """Delete RULE and return True if there was a change.""" if compile.is_atom(rule): rule = compile.Rule(rule, [], rule.location) self.log(rule.head.table, "Delete: %s", rule) return self.rules.discard_rule(rule.head.table, rule) def content(self, tablenames=None): if tablenames is None: tablenames = self.rules.keys() results = [] for table in tablenames: if table in self.rules: results.extend(self.rules.get_rules(table)) return results def head_index(self, table, match_literal=None): """Return head index. This routine must return all the formulas pertinent for top-down evaluation when a literal with TABLE is at the top of the stack. """ if table in self.rules: return self.rules.get_rules(table, match_literal) return [] def arity(self, tablename): """Return the number of arguments TABLENAME takes. None if unknown because TABLENAME is not defined here. """ # assuming a fixed arity for all tables formulas = self.head_index(tablename) if len(formulas) == 0: return None first = formulas[0] # should probably have an overridable function for computing # the arguments of a head. Instead we assume heads have .arguments return len(self.head(first).arguments) def defined_tablenames(self): """Returns list of table names defined in/written to this theory.""" return self.rules.keys() def head(self, formula): """Given the output from head_index(), return the formula head. Given a FORMULA, return the thing to unify against. Usually, FORMULA is a compile.Rule, but it could be anything returned by HEAD_INDEX. """ return formula.head def body(self, formula): """Return formula body. Given a FORMULA, return a list of things to push onto the top-down eval stack. """ return formula.body
def setUp(self): super(TestRuleSet, self).setUp() self.ruleset = RuleSet()
class TestRuleSet(base.TestCase): def setUp(self): super(TestRuleSet, self).setUp() self.ruleset = RuleSet() def test_empty_ruleset(self): self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_clear_ruleset(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') self.ruleset.add_rule('p', rule1) self.ruleset.clear() self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_add_rule(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') self.assertTrue(self.ruleset.add_rule('p', rule1)) self.assertTrue('p' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) def test_add_existing_rule(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') self.assertTrue(self.ruleset.add_rule('p', rule1)) self.assertTrue('p' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) self.assertFalse(self.ruleset.add_rule('p', rule1)) self.assertTrue('p' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) def test_add_rules_with_same_head(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') rule2 = compile.parse1('p(x,y) :- s(x), t(y)') self.assertTrue(self.ruleset.add_rule('p', rule1)) self.assertTrue('p' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) self.assertTrue(self.ruleset.add_rule('p', rule2)) self.assertTrue('p' in self.ruleset) self.assertTrue(rule1 in self.ruleset.get_rules('p')) self.assertTrue(rule2 in self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) def test_add_rules_with_different_head(self): rule1 = compile.parse1('p1(x,y) :- q(x), r(y)') rule2 = compile.parse1('p2(x,y) :- s(x), t(y)') self.assertTrue(self.ruleset.add_rule('p1', rule1)) self.assertTrue(self.ruleset.add_rule('p2', rule2)) self.assertTrue('p1' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p1')) self.assertTrue('p1' in self.ruleset.keys()) self.assertTrue('p2' in self.ruleset) self.assertEqual([rule2], self.ruleset.get_rules('p2')) self.assertTrue('p2' in self.ruleset.keys()) def test_discard_rule(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') self.assertTrue(self.ruleset.add_rule('p', rule1)) self.assertTrue('p' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p')) self.assertTrue(self.ruleset.discard_rule('p', rule1)) self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_discard_nonexistent_rule(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') self.assertFalse(self.ruleset.discard_rule('p', rule1)) self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_discard_rules_with_same_head(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') rule2 = compile.parse1('p(x,y) :- s(x), t(y)') self.assertTrue(self.ruleset.add_rule('p', rule1)) self.assertTrue(self.ruleset.add_rule('p', rule2)) self.assertTrue('p' in self.ruleset) self.assertTrue(rule1 in self.ruleset.get_rules('p')) self.assertTrue(rule2 in self.ruleset.get_rules('p')) self.assertTrue(self.ruleset.discard_rule('p', rule1)) self.assertTrue(self.ruleset.discard_rule('p', rule2)) self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_discard_rules_with_different_head(self): rule1 = compile.parse1('p1(x,y) :- q(x), r(y)') rule2 = compile.parse1('p2(x,y) :- s(x), t(y)') self.assertTrue(self.ruleset.add_rule('p1', rule1)) self.assertTrue(self.ruleset.add_rule('p2', rule2)) self.assertTrue('p1' in self.ruleset) self.assertTrue('p2' in self.ruleset) self.assertTrue(rule1 in self.ruleset.get_rules('p1')) self.assertTrue(rule2 in self.ruleset.get_rules('p2')) self.assertTrue(self.ruleset.discard_rule('p1', rule1)) self.assertTrue(self.ruleset.discard_rule('p2', rule2)) self.assertFalse('p1' in self.ruleset) self.assertFalse('p2' in self.ruleset) self.assertEqual([], self.ruleset.keys())
class NonrecursiveRuleTheory(TopDownTheory): """A non-recursive collection of Rules.""" def __init__(self, name=None, abbr=None, schema=None, theories=None): super(NonrecursiveRuleTheory, self).__init__(name=name, abbr=abbr, theories=theories, schema=schema) # dictionary from table name to list of rules with that table in head self.rules = RuleSet() self.kind = NONRECURSIVE_POLICY_TYPE # External Interface # SELECT implemented by TopDownTheory def insert(self, rule): changes = self.update([Event(formula=rule, insert=True)]) return [event.formula for event in changes] def delete(self, rule): changes = self.update([Event(formula=rule, insert=False)]) return [event.formula for event in changes] def update(self, events): """Apply EVENTS. And return the list of EVENTS that actually changed the theory. Each event is the insert or delete of a policy statement. """ changes = [] self.log(None, "Update %s", iterstr(events)) try: for event in events: formula = compile.reorder_for_safety(event.formula) if event.insert: if self.insert_actual(formula): changes.append(event) else: if self.delete_actual(formula): changes.append(event) except Exception as e: LOG.exception("runtime caught an exception") raise e return changes def update_would_cause_errors(self, events): """Return a list of compile.CongressException. Return a list of compile.CongressException if we were to apply the insert/deletes of policy statements dictated by EVENTS to the current policy. """ self.log(None, "update_would_cause_errors %s", iterstr(events)) errors = [] for event in events: if not compile.is_datalog(event.formula): errors.append( compile.CongressException("Non-formula found: {}".format( str(event.formula)))) else: 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)) # Would also check that rules are non-recursive, but that # is currently being handled by Runtime. The current implementation # disallows recursion in all theories. return errors def define(self, rules): """Empties and then inserts RULES.""" self.empty() return self.update( [Event(formula=rule, insert=True) for rule in rules]) def empty(self): """Deletes contents of theory.""" self.rules.clear() def policy(self): # eliminate all rules with empty bodies return [p for p in self.content() if len(p.body) > 0] def get_arity_self(self, tablename): if tablename not in self.rules: return None if len(self.rules.get_rules(tablename)) == 0: return None return len(list(self.rules.get_rules(tablename))[0].head.arguments) def __contains__(self, formula): return formula in self.rules # Internal Interface def insert_actual(self, rule): """Insert RULE and return True if there was a change.""" if compile.is_atom(rule): rule = compile.Rule(rule, [], rule.location) self.log(rule.head.table, "Insert: %s", rule) return self.rules.add_rule(rule.head.table, rule) def delete_actual(self, rule): """Delete RULE and return True if there was a change.""" if compile.is_atom(rule): rule = compile.Rule(rule, [], rule.location) self.log(rule.head.table, "Delete: %s", rule) return self.rules.discard_rule(rule.head.table, rule) def content(self, tablenames=None): if tablenames is None: tablenames = self.rules.keys() results = [] for table in tablenames: if table in self.rules: results.extend(self.rules.get_rules(table)) return results