congress/congress/datalog/database.py

414 lines
14 KiB
Python

# Copyright (c) 2015 VMware, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
from six.moves import range
from congress.datalog import base
from congress.datalog import compile
from congress.datalog import topdown
from congress.datalog import unify
from congress.datalog import utility
from congress import exception
##############################################################################
# Concrete Theory: Database
##############################################################################
class Database(topdown.TopDownTheory):
class Proof(object):
def __init__(self, binding, rule):
self.binding = binding
self.rule = rule
def __str__(self):
return "apply({}, {})".format(str(self.binding), str(self.rule))
def __eq__(self, other):
result = (self.binding == other.binding and
self.rule == other.rule)
# LOG.debug("Pf: Comparing %s and %s: %s", self, other, result)
# LOG.debug("Pf: %s == %s is %s",
# self.binding, other.binding, self.binding == other.binding)
# LOG.debug("Pf: %s == %s is %s",
# self.rule, other.rule, self.rule == other.rule)
return result
def __ne__(self, other):
return not self.__eq__(other)
class ProofCollection(object):
def __init__(self, proofs):
self.contents = list(proofs)
def __str__(self):
return '{' + ",".join(str(x) for x in self.contents) + '}'
def __isub__(self, other):
if other is None:
return
# LOG.debug("PC: Subtracting %s and %s", self, other)
remaining = []
for proof in self.contents:
if proof not in other.contents:
remaining.append(proof)
self.contents = remaining
return self
def __ior__(self, other):
if other is None:
return
# LOG.debug("PC: Unioning %s and %s", self, other)
for proof in other.contents:
# LOG.debug("PC: Considering %s", proof)
if proof not in self.contents:
self.contents.append(proof)
return self
def __getitem__(self, key):
return self.contents[key]
def __len__(self):
return len(self.contents)
def __ge__(self, iterable):
for proof in iterable:
if proof not in self.contents:
# LOG.debug("Proof %s makes %s not >= %s",
# proof, self, iterstr(iterable))
return False
return True
def __le__(self, iterable):
for proof in self.contents:
if proof not in iterable:
# LOG.debug("Proof %s makes %s not <= %s",
# proof, self, iterstr(iterable))
return False
return True
def __eq__(self, other):
return self <= other and other <= self
def __ne__(self, other):
return not self.__eq__(other)
class DBTuple(object):
def __init__(self, iterable, proofs=None):
self.tuple = tuple(iterable)
if proofs is None:
proofs = []
self.proofs = Database.ProofCollection(proofs)
def __eq__(self, other):
return self.tuple == other.tuple
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
return str(self.tuple) + str(self.proofs)
def __len__(self):
return len(self.tuple)
def __getitem__(self, index):
return self.tuple[index]
def __setitem__(self, index, value):
self.tuple[index] = value
def match(self, atom, unifier):
# LOG.debug("DBTuple matching %s against atom %s in %s",
# self, iterstr(atom.arguments), unifier)
if len(self.tuple) != len(atom.arguments):
return None
changes = []
for i in range(0, len(atom.arguments)):
val, binding = unifier.apply_full(atom.arguments[i])
# LOG.debug("val(%s)=%s at %s; comparing to object %s",
# atom.arguments[i], val, binding, self.tuple[i])
if val.is_variable():
changes.append(binding.add(
val, compile.Term.create_from_python(self.tuple[i]),
None))
else:
if val.name != self.tuple[i]:
unify.undo_all(changes)
return None
return changes
def __init__(self, name=None, abbr=None, theories=None, schema=None,
desc=None, owner=None):
super(Database, self).__init__(
name=name, abbr=abbr, theories=theories, schema=schema,
desc=desc, owner=owner)
self.data = {}
self.kind = base.DATABASE_POLICY_TYPE
def str2(self):
def hash2str(h):
s = "{"
s += ", ".join(["{} : {}".format(str(key), str(h[key]))
for key in h])
return s
def hashlist2str(h):
strings = []
for key in h:
s = "{} : ".format(key)
s += '['
s += ', '.join([str(val) for val in h[key]])
s += ']'
strings.append(s)
return '{' + ", ".join(strings) + '}'
return hashlist2str(self.data)
def __eq__(self, other):
return self.data == other.data
def __ne__(self, other):
return not self.__eq__(other)
def __sub__(self, other):
def add_tuple(table, dbtuple):
new = [table]
new.extend(dbtuple.tuple)
results.append(new)
results = []
for table in self.data:
if table not in other.data:
for dbtuple in self.data[table]:
add_tuple(table, dbtuple)
else:
for dbtuple in self.data[table]:
if dbtuple not in other.data[table]:
add_tuple(table, dbtuple)
return results
def __or__(self, other):
def add_db(db):
for table in db.data:
for dbtuple in db.data[table]:
result.insert(compile.Literal.create_from_table_tuple(
table, dbtuple.tuple), proofs=dbtuple.proofs)
result = Database()
add_db(self)
add_db(other)
return result
def __getitem__(self, key):
# KEY must be a tablename
return self.data[key]
def content(self, tablenames=None):
"""Return a sequence of Literals representing all the table data."""
results = []
if tablenames is None:
tablenames = self.data.keys()
for table in tablenames:
if table not in self.data:
continue
for dbtuple in self.data[table]:
results.append(compile.Literal.create_from_table_tuple(
table, dbtuple.tuple))
return results
def is_noop(self, event):
"""Returns T if EVENT is a noop on the database."""
# insert/delete same code but with flipped return values
# Code below is written as insert, except noop initialization.
if event.is_insert():
noop = True
else:
noop = False
if event.formula.table.table not in self.data:
return not noop
event_data = self.data[event.formula.table.table]
raw_tuple = tuple(event.formula.argument_names())
for dbtuple in event_data:
if dbtuple.tuple == raw_tuple:
if event.proofs <= dbtuple.proofs:
return noop
return not noop
def __contains__(self, formula):
if not compile.is_atom(formula):
return False
if formula.table.table not in self.data:
return False
event_data = self.data[formula.table.table]
raw_tuple = tuple(formula.argument_names())
return any((dbtuple.tuple == raw_tuple for dbtuple in event_data))
def explain(self, atom):
if atom.table.table not in self.data or not atom.is_ground():
return self.ProofCollection([])
args = tuple([x.name for x in atom.arguments])
for dbtuple in self.data[atom.table.table]:
if dbtuple.tuple == args:
return dbtuple.proofs
def tablenames(self, body_only=False, include_builtin=False,
include_modal=True):
"""Return all table names occurring in this theory."""
if body_only:
return []
return self.data.keys()
# overloads for TopDownTheory so we can properly use the
# top_down_evaluation routines
def defined_tablenames(self):
return self.data.keys()
def head_index(self, table, match_literal=None):
if table not in self.data:
return []
return self.data[table]
def head(self, thing):
return thing
def body(self, thing):
return []
def bi_unify(self, dbtuple, unifier1, atom, unifier2, theoryname):
"""THING1 is always a ground DBTuple and THING2 is always an ATOM."""
return dbtuple.match(atom, unifier2)
def atom_to_internal(self, atom, proofs=None):
return atom.table.table, self.DBTuple(atom.argument_names(), proofs)
def insert(self, atom, proofs=None):
"""Inserts ATOM into the DB. Returns changes."""
return self.modify(compile.Event(formula=atom, insert=True,
proofs=proofs))
def delete(self, atom, proofs=None):
"""Deletes ATOM from the DB. Returns changes."""
return self.modify(compile.Event(formula=atom, insert=False,
proofs=proofs))
def update(self, events):
"""Applies all of EVENTS to the DB.
Each event is either an insert or a delete.
"""
changes = []
for event in events:
changes.extend(self.modify(event))
return changes
def update_would_cause_errors(self, events):
"""Return a list of Policyxception.
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", utility.iterstr(events))
errors = []
for event in events:
if not compile.is_atom(event.formula):
errors.append(exception.PolicyException(
"Non-atomic formula is not permitted: {}".format(
str(event.formula))))
else:
errors.extend(compile.fact_errors(
event.formula, self.theories, self.name))
return errors
def modify(self, event):
"""Insert/Delete atom.
Inserts/deletes ATOM and returns a list of changes that
were caused. That list contains either 0 or 1 Event.
"""
assert compile.is_atom(event.formula), "Modify requires Atom"
atom = event.formula
self.log(atom.table.table, "Modify: %s", atom)
if self.is_noop(event):
self.log(atom.table.table, "Event %s is a noop", event)
return []
if event.insert:
self.insert_actual(atom, proofs=event.proofs)
else:
self.delete_actual(atom, proofs=event.proofs)
return [event]
def insert_actual(self, atom, proofs=None):
"""Workhorse for inserting ATOM into the DB.
Along with proofs explaining how ATOM was computed from other tables.
"""
assert compile.is_atom(atom), "Insert requires Atom"
table, dbtuple = self.atom_to_internal(atom, proofs)
self.log(table, "Insert: %s", atom)
if table not in self.data:
self.data[table] = [dbtuple]
self.log(atom.table.table, "First tuple in table %s", table)
return
else:
for existingtuple in self.data[table]:
assert existingtuple.proofs is not None
if existingtuple.tuple == dbtuple.tuple:
assert existingtuple.proofs is not None
existingtuple.proofs |= dbtuple.proofs
assert existingtuple.proofs is not None
return
self.data[table].append(dbtuple)
def delete_actual(self, atom, proofs=None):
"""Workhorse for deleting ATOM from the DB.
Along with the proofs that are no longer true.
"""
assert compile.is_atom(atom), "Delete requires Atom"
self.log(atom.table.table, "Delete: %s", atom)
table, dbtuple = self.atom_to_internal(atom, proofs)
if table not in self.data:
return
for i in range(0, len(self.data[table])):
existingtuple = self.data[table][i]
if existingtuple.tuple == dbtuple.tuple:
existingtuple.proofs -= dbtuple.proofs
if len(existingtuple.proofs) == 0:
del self.data[table][i]
return
def policy(self):
"""Return the policy for this theory.
No policy in this theory; only data.
"""
return []
def get_arity_self(self, tablename):
if tablename not in self.data:
return None
if len(self.data[tablename]) == 0:
return None
return len(self.data[tablename][0].tuple)
def content_string(self):
s = ""
for lit in self.content():
s += str(lit) + '\n'
return s + '\n'