Merge "Pad positional args up to required number"

This commit is contained in:
Jenkins 2017-04-13 18:46:08 +00:00 committed by Gerrit Code Review
commit c707d2bd32
3 changed files with 197 additions and 95 deletions

View File

@ -800,84 +800,96 @@ class Literal (object):
self.table.drop_service()
return self
def eliminate_column_references(self, theories, default_theory=None,
index=0, prefix=''):
"""Expand column references to traditional datalog positional args.
def eliminate_column_references_and_pad_positional(
self, theories, default_theory=None, index=0, prefix=''):
"""Expand column references to positional args and pad positional args.
Returns a new literal, unless no column references.
Expand column references to traditional datalog positional args.
Also pad positional args if too few are provided.
Returns a new literal. If no column reference, unless no schema found
for the table.
"""
# TODO(ekcs): remove unused parameter: index
# corner cases
if len(self.named_arguments) == 0:
return self
theory = literal_theory(self, theories, default_theory)
if theory is None or theory.schema is None:
raise exception.IncompleteSchemaException(
"Literal %s uses named arguments, but the "
"schema is unknown." % self)
if theory.kind != base.DATASOURCE_POLICY_TYPE: # eventually remove
raise exception.PolicyException(
"Literal {} uses column references, but '{}' does not "
"reference a datasource policy.".format(self, theory.name))
schema = theory.schema
if self.table.table not in schema:
raise exception.IncompleteSchemaException(
"Literal {} uses unknown table {}.".format(
str(self), str(self.table.table)))
if len(self.named_arguments) > 0:
theory = literal_theory(self, theories, default_theory)
if theory is None or theory.schema is None:
raise exception.IncompleteSchemaException(
"Literal %s uses named arguments, but the "
"schema is unknown." % self)
if theory.kind != base.DATASOURCE_POLICY_TYPE: # eventually remove
raise exception.PolicyException(
"Literal {} uses column references, but '{}' does not "
"reference a datasource policy.".format(self, theory.name))
schema = theory.schema
if self.table.table not in schema:
raise exception.IncompleteSchemaException(
"Literal {} uses unknown table {}.".format(
str(self), str(self.table.table)))
# check if named arguments conflict with positional or named arguments
errors = []
term_index = {}
for col, arg in self.named_arguments.items():
if isinstance(col, six.string_types): # column name
index = schema.column_number(self.table.table, col)
if index is None:
errors.append(exception.PolicyException(
"In literal {} column name {} does not exist".format(
str(self), col)))
continue
if index < len(self.arguments):
errors.append(exception.PolicyException(
"In literal {} column name {} references position {},"
" which is already provided by position.".format(
str(self), col, index)))
if index in self.named_arguments:
errors.append(exception.PolicyException(
"In literal {} column name {} references position {}, "
"which is also referenced by number.))".format(
str(self), col, index)))
if index in term_index:
# should have already caught this case above
errors.append(exception.PolicyException(
"In literal {}, column name {} references position {},"
" which already has reference {}".format(
str(self), col, index, str(term_index[index]))))
term_index[index] = arg
else: # column number
if col >= schema.arity(self.table.table):
errors.append(exception.PolicyException(
"In literal {} column index {} is too large".format(
str(self), col)))
if col < len(self.arguments):
errors.append(exception.PolicyException(
"In literal {} column index {} "
" is already provided by position.".format(
str(self), col)))
name = schema.column_name(self.table.table, col)
if name in self.named_arguments:
errors.append(exception.PolicyException(
"In literal {} column index {} references column {}, "
"which is also referenced by name.))".format(
str(self), col, name)))
if col in term_index:
# should have already caught this case above
errors.append(exception.PolicyException(
"In literal {} column index {} already has a reference"
" {}".format(str(self), col, str(term_index[col]))))
term_index[col] = arg
if errors:
raise exception.PolicyException(
" ".join(str(err) for err in errors))
# check if named arguments conflict with positional or named args
errors = []
term_index = {}
for col, arg in self.named_arguments.items():
if isinstance(col, six.string_types): # column name
index = schema.column_number(self.table.table, col)
if index is None:
errors.append(exception.PolicyException(
"In literal {} column name {} does not "
"exist".format(str(self), col)))
continue
if index < len(self.arguments):
errors.append(exception.PolicyException(
"In literal {} column name {} references position "
"{}, which is already provided by "
"position.".format(str(self), col, index)))
if index in self.named_arguments:
errors.append(exception.PolicyException(
"In literal {} column name {} references position "
"{}, which is also referenced by number.))".format(
str(self), col, index)))
if index in term_index:
# should have already caught this case above
errors.append(exception.PolicyException(
"In literal {}, column name {} references "
"position {}, which already has reference "
"{}".format(str(self), col, index,
str(term_index[index]))))
term_index[index] = arg
else: # column number
if col >= schema.arity(self.table.table):
errors.append(exception.PolicyException(
"In literal {} column index {} is too "
"large".format(str(self), col)))
if col < len(self.arguments):
errors.append(exception.PolicyException(
"In literal {} column index {} "
" is already provided by position.".format(
str(self), col)))
name = schema.column_name(self.table.table, col)
if name in self.named_arguments:
errors.append(exception.PolicyException(
"In literal {} column index {} references column "
"{}, which is also referenced by name.))".format(
str(self), col, name)))
if col in term_index:
# should have already caught this case above
errors.append(exception.PolicyException(
"In literal {} column index {} already has a "
"reference {}".format(
str(self), col, str(term_index[col]))))
term_index[col] = arg
if errors:
raise exception.PolicyException(
" ".join(str(err) for err in errors))
else:
theory = literal_theory(self, theories, default_theory)
if theory is None or theory.schema is None:
return self
schema = theory.schema
if self.table.table not in schema:
return self
term_index = {}
# turn reference args into position args
position_args = list(self.arguments) # copy the original list
@ -1072,17 +1084,21 @@ class Rule(object):
def is_update(self):
return self.head.is_update()
def eliminate_column_references(self, theories, default_theory=None):
"""Return version of SELF where all column references have been removed.
def eliminate_column_references_and_pad_positional(
self, theories, default_theory=None):
"""Return version of SELF /w col refs removed and pos args padded.
All column references removed. Positional args padded up to required
length.
Throws exception if RULE is inconsistent with schemas.
"""
pre = self._unused_variable_prefix()
heads = []
for i in range(0, len(self.heads)):
heads.append(self.heads[i].eliminate_column_references(
theories, default_theory=default_theory,
index=i, prefix='%s%s' % (pre, i)))
heads.append(
self.heads[i].eliminate_column_references_and_pad_positional(
theories, default_theory=default_theory,
index=i, prefix='%s%s' % (pre, i)))
body = []
sorted_lits = sorted(self.body)
@ -1091,9 +1107,10 @@ class Rule(object):
lit_rank[sorted_lits[i]] = i
for i in range(0, len(self.body)):
body.append(self.body[i].eliminate_column_references(
theories, default_theory=default_theory,
index=i, prefix='%s%s' % (pre, lit_rank[self.body[i]])))
body.append(
self.body[i].eliminate_column_references_and_pad_positional(
theories, default_theory=default_theory,
index=i, prefix='%s%s' % (pre, lit_rank[self.body[i]])))
return Rule(heads, body, self.location, name=self.name,
comment=self.comment, original_str=self.original_str)

View File

@ -1056,18 +1056,20 @@ class Runtime (object):
enabled = []
errors = []
for event in events:
errs = compile.check_schema_consistency(
event.formula, self.theory, event.target)
if len(errs) > 0:
errors.append((event, errs))
continue
try:
oldformula = event.formula
event.formula = oldformula.eliminate_column_references(
self.theory, default_theory=event.target)
event.formula = \
oldformula.eliminate_column_references_and_pad_positional(
self.theory, default_theory=event.target)
# doesn't copy over ID since it creates a new one
event.formula.set_id(oldformula.id)
enabled.append(event)
errs = compile.check_schema_consistency(
event.formula, self.theory, event.target)
if len(errs) > 0:
errors.append((event, errs))
continue
except exception.IncompleteSchemaException as e:
if persistent:
# FIXME(ekcs): inconsistent behavior?

View File

@ -264,6 +264,83 @@ class TestColumnReferences(base.TestCase):
'1 is already provided by position arguments',
'Conflict between name and position')
def test_positional_args_padding_atom(self):
"""Test positional args padding on a single atom."""
def check_err(rule, errmsg, msg):
rule = compile.parse1(rule)
try:
rule.eliminate_column_references_and_pad_positional(theories)
self.fail("Failed to throw error {}".format(errmsg))
except (exception.PolicyException,
exception.IncompleteSchemaException) as e:
emsg = "Err messages '{}' should include '{}'".format(
str(e), errmsg)
self.assertIn(errmsg, str(e), msg + ": " + emsg)
def check(code, correct, msg, no_theory=False):
actual = compile.parse1(
code).eliminate_column_references_and_pad_positional(
{} if no_theory else theories)
eq = helper.datalog_same(str(actual), correct)
self.assertTrue(eq, msg)
run = agnostic.Runtime()
run.create_policy('nova')
schema = compile.Schema({'q': ('id', 'name', 'status')})
theories = {'nova': self.SchemaWrapper(schema)}
# Too few positional args
code = ("p(x) :- nova:q(w, y)")
correct = "p(x) :- nova:q(w, y, x3)"
check(code, correct, 'Too few positional args')
code = ("p(x) :- nova:q(w)")
correct = "p(x) :- nova:q(w, y, x3)"
check(code, correct, 'Too few positional args')
code = ("p(x) :- nova:q()")
correct = "p(x) :- nova:q(w, y, x3)"
check(code, correct, 'Too few (no) positional args')
# No schema provided, no change
code = ("p(x) :- nova:q(w, y)")
correct = "p(x) :- nova:q(w, y)"
check(code, correct, 'No schema provided', True)
code = ("p(x) :- nova:q(w, x, y, z)")
correct = "p(x) :- nova:q(w, x, y, z)"
check(code, correct, 'No schema provided', True)
def test_positional_args_padding_multiple_atoms(self):
"""Test positional args padding on a single atom."""
def check(code, correct, msg, no_theory=False):
actual = compile.parse1(
code).eliminate_column_references_and_pad_positional(
{} if no_theory else theories)
eq = helper.datalog_same(str(actual), correct)
self.assertTrue(eq, msg)
run = agnostic.Runtime()
run.create_policy('nova')
schema = compile.Schema({'q': ('id', 'name', 'status'),
'r': ('id', 'age', 'weight')})
theories = {'nova': self.SchemaWrapper(schema)}
# Multiple atoms, no shared variable
code = ("p(x) :- nova:q(x, y), nova:r(w)")
correct = "p(x) :- nova:q(x, y, z0), nova:r(w, y0, y1)"
check(code, correct, 'Multiple atoms')
# Multiple atoms, some shared variable
code = ("p(x) :- nova:q(x, y), nova:r(x)")
correct = "p(x) :- nova:q(x, y, z0), nova:r(x, y0, y1)"
check(code, correct, 'Multiple atoms')
# Multiple atoms, same table
code = ("p(x) :- nova:q(x, y), nova:q(x)")
correct = "p(x) :- nova:q(x, y, z0), nova:q(x, w0, w1)"
check(code, correct, 'Multiple atoms, same table')
def test_column_references_validation_errors(self):
"""Test invalid column references occurring in a single atom."""
schema = compile.Schema({'q': ('id', 'name', 'status'),
@ -274,7 +351,7 @@ class TestColumnReferences(base.TestCase):
def check_err(rule, errmsg, msg):
rule = compile.parse1(rule)
try:
rule.eliminate_column_references(theories)
rule.eliminate_column_references_and_pad_positional(theories)
self.fail("Failed to throw error {}".format(errmsg))
except (exception.PolicyException,
exception.IncompleteSchemaException) as e:
@ -316,7 +393,8 @@ class TestColumnReferences(base.TestCase):
def test_column_references_atom(self):
"""Test column references occurring in a single atom in a rule."""
def check(code, correct, msg):
actual = compile.parse1(code).eliminate_column_references(theories)
actual = compile.parse1(
code).eliminate_column_references_and_pad_positional(theories)
eq = helper.datalog_same(str(actual), correct)
self.assertTrue(eq, msg)
@ -376,10 +454,13 @@ class TestColumnReferences(base.TestCase):
correct = "p(x) :- nova:q(x, y, z)"
check(code, correct, 'Pure positional without schema')
# Too few pure positional EKCS
def test_column_references_multiple_atoms(self):
"""Test column references occurring in multiple atoms in a rule."""
def check(code, correct, msg):
actual = compile.parse1(code).eliminate_column_references(theories)
actual = compile.parse1(
code).eliminate_column_references_and_pad_positional(theories)
eq = helper.datalog_same(str(actual), correct)
self.assertTrue(eq, msg)
@ -412,10 +493,12 @@ class TestColumnReferences(base.TestCase):
'r': ('id', 'age', 'weight')})
theories = {'nova': self.SchemaWrapper(schema)}
rule1 = compile.parse1("p(x) :- nova:q(id=x, 2=y), nova:r(id=x)"
).eliminate_column_references(theories)
rule2 = compile.parse1("p(x) :- nova:r(id=x), nova:q(id=x, 2=y)"
).eliminate_column_references(theories)
rule1 = compile.parse1(
"p(x) :- nova:q(id=x, 2=y), nova:r(id=x)"
).eliminate_column_references_and_pad_positional(theories)
rule2 = compile.parse1(
"p(x) :- nova:r(id=x), nova:q(id=x, 2=y)"
).eliminate_column_references_and_pad_positional(theories)
self.assertEqual(rule1, rule2, 'eliminate_column_references failed to '
'preserve order insensitivity')