# HG changeset patch # User Sylvain Thénault <sylvain.thenault@logilab.fr> # Date 1274860290 -7200 # Wed May 26 09:51:30 2010 +0200 # Branch stable # Node ID 65393ec1836a2df508237b3f8a9c0923306025b0 # Parent fcce1537edb3513e7d807cf739a4cd7a7642727e normalize NOT to NOT EXISTS when that's the actual meaning of the query. Ease later scope handling. diff --git a/ChangeLog b/ChangeLog --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,10 @@ ChangeLog for RQL ================= +-- + * normalize NOT() to NOT EXISTS() when it makes sense + + 2010-04-20 -- 0.26.0 * setuptools support diff --git a/base.py b/base.py --- a/base.py +++ b/base.py @@ -57,13 +57,6 @@ """ return self.parent.scope - @property - def sqlscope(self): - """Return the SQL scope node to which this node belong (eg Select, - Exists or Not node) - """ - return self.parent.sqlscope - def get_nodes(self, klass): """Return the list of nodes of a given class in the subtree. diff --git a/nodes.py b/nodes.py --- a/nodes.py +++ b/nodes.py @@ -268,10 +268,6 @@ def __repr__(self, encoding=None, kwargs=None): return 'NOT (%s)' % repr(self.children[0]) - @property - def sqlscope(self): - return self - def ored(self, traverse_scope=False, _fromnode=None): # XXX consider traverse_scope ? return self.parent.ored(traverse_scope, _fromnode or self) @@ -338,7 +334,6 @@ @property def scope(self): return self - sqlscope = scope def ored(self, traverse_scope=False, _fromnode=None): if not traverse_scope: @@ -1000,8 +995,6 @@ def get_scope(self): return self.query scope = property(get_scope, set_scope) - sqlscope = scope - set_sqlscope = set_scope class Variable(Referenceable): @@ -1029,7 +1022,6 @@ def prepare_annotation(self): super(Variable, self).prepare_annotation() self.stinfo['scope'] = None - self.stinfo['sqlscope'] = None def _set_scope(self, key, scopenode): if scopenode is self.stmt or self.stinfo[key] is None: @@ -1043,12 +1035,6 @@ return self.stinfo['scope'] scope = property(get_scope, set_scope) - def set_sqlscope(self, sqlscopenode): - self._set_scope('sqlscope', sqlscopenode) - def get_sqlscope(self): - return self.stinfo['sqlscope'] - sqlscope = property(get_sqlscope, set_sqlscope) - def valuable_references(self): """return the number of "valuable" references : references is in selection or in a non type (is) relations diff --git a/stcheck.py b/stcheck.py --- a/stcheck.py +++ b/stcheck.py @@ -15,9 +15,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with rql. If not, see <http://www.gnu.org/licenses/>. -"""RQL Syntax tree annotator. +"""RQL Syntax tree annotator""" -""" __docformat__ = "restructuredtext en" from itertools import chain @@ -27,7 +26,7 @@ from rql._exceptions import BadRQLQuery from rql.utils import function_description -from rql.nodes import (VariableRef, Constant, Not, Exists, Function, +from rql.nodes import (Relation, VariableRef, Constant, Not, Exists, Function, Variable, variable_refs) from rql.stmts import Union @@ -310,6 +309,37 @@ state.under_not.append(True) def leave_not(self, not_, state): state.under_not.pop() + # NOT normalization + child = not_.children[0] + if self._should_wrap_by_exists(child): + not_.remove(child) + not_.append(Exists(child)) + + def _should_wrap_by_exists(self, child): + if isinstance(child, Exists): + return False + if not isinstance(child, Relation): + return True + if child.r_type == 'identity': + return False + rschema = self.schema.rschema(child.r_type) + if rschema.final: + return False + # XXX no exists for `inlined` relation (allow IS NULL optimization) + # unless the lhs variable is only referenced from this neged relation, + # in which case it's *not* in the statement's scope, hence EXISTS should + # be added anyway + if rschema.inlined: + references = child.children[0].variable.references() + valuable = 0 + for vref in references: + rel = vref.relation() + if rel is None or not rel.is_types_restriction(): + if valuable: + return False + valuable = 1 + return True + return not child.is_types_restriction() def visit_relation(self, relation, state): if relation.optional and state.under_not: @@ -349,6 +379,7 @@ '(use IN for %s if desired)' % lhsvar.name) else: state.add_var_info(lhsvar, VAR_HAS_UID_REL) + for vref in relation.children[1].get_nodes(VariableRef): state.add_var_info(vref.variable, VAR_HAS_REL) try: @@ -454,9 +485,8 @@ for vref in term.get_nodes(VariableRef): vref.variable.stinfo['selected'].add(i) vref.variable.set_scope(node) - vref.variable.set_sqlscope(node) if node.where is not None: - node.where.accept(self, node, node) + node.where.accept(self, node) visit_insert = visit_delete = visit_set = _visit_stmt @@ -538,23 +568,23 @@ sol[newvar.name] = sol[var.name] rel = exists.add_relation(var, 'identity', newvar) # we have to force visit of the introduced relation - self.visit_relation(rel, exists, exists) + self.visit_relation(rel, exists) return newvar # tree nodes ############################################################## - def visit_exists(self, node, scope, sqlscope): - node.children[0].accept(self, node, node) + def visit_exists(self, node, scope): + node.children[0].accept(self, node) - def visit_not(self, node, scope, sqlscope): - node.children[0].accept(self, scope, node) + def visit_not(self, node, scope): + node.children[0].accept(self, scope) - def visit_and(self, node, scope, sqlscope): - node.children[0].accept(self, scope, sqlscope) - node.children[1].accept(self, scope, sqlscope) + def visit_and(self, node, scope): + node.children[0].accept(self, scope) + node.children[1].accept(self, scope) visit_or = visit_and - def visit_relation(self, relation, scope, sqlscope): + def visit_relation(self, relation, scope): #assert relation.parent, repr(relation) lhs, rhs = relation.get_parts() # may be a constant once rqlst has been simplified @@ -591,7 +621,6 @@ rschema = self.schema.rschema(rtype) if lhsvar is not None: lhsvar.set_scope(scope) - lhsvar.set_sqlscope(sqlscope) lhsvar.stinfo['relations'].add(relation) if rtype in self.special_relations: key = '%srels' % self.special_relations[rtype] @@ -609,7 +638,6 @@ for vref in rhs.get_nodes(VariableRef): var = vref.variable var.set_scope(scope) - var.set_sqlscope(sqlscope) var.stinfo['relations'].add(relation) var.stinfo['rhsrelations'].add(relation) if vref is rhs.children[0] and rschema.final: diff --git a/stmts.py b/stmts.py --- a/stmts.py +++ b/stmts.py @@ -168,7 +168,6 @@ @property def scope(self): return self - sqlscope = scope def ored(self, traverse_scope=False, _fromnode=None): return None diff --git a/test/unittest_analyze.py b/test/unittest_analyze.py --- a/test/unittest_analyze.py +++ b/test/unittest_analyze.py @@ -301,7 +301,7 @@ {'X': 'Student', 'T': 'Eetype'}]) def test_not(self): - node = self.helper.parse('Any X WHERE not X is Person') + node = self.helper.parse('Any X WHERE NOT X is Person') self.helper.compute_solutions(node, debug=DEBUG) sols = sorted(node.children[0].solutions) expected = ALL_SOLS[:] diff --git a/test/unittest_stcheck.py b/test/unittest_stcheck.py --- a/test/unittest_stcheck.py +++ b/test/unittest_stcheck.py @@ -226,7 +226,7 @@ newroot.append(copy) self.annotate(newroot) self.simplify(newroot) - self.assertEquals(newroot.as_string(), 'Any 1,U WHERE 2 owned_by U, NOT 1 owned_by U') + self.assertEquals(newroot.as_string(), 'Any 1,U WHERE 2 owned_by U, NOT EXISTS(1 owned_by U)') self.assertEquals(copy.defined_vars['U'].valuable_references(), 3) @@ -241,23 +241,60 @@ # self.annotate(rqlst) # self.failUnless(rqlst.defined_vars['L'].stinfo['attrvar']) - def test_is_rel_no_scope(self): - """is relation used as type restriction should not affect variable's scope, - and should not be included in stinfo['relations']""" + def test_is_rel_no_scope_1(self): + """is relation used as type restriction should not affect variable's + scope, and should not be included in stinfo['relations'] + """ rqlst = self.parse('Any X WHERE C is Company, EXISTS(X work_for C)').children[0] C = rqlst.defined_vars['C'] self.failIf(C.scope is rqlst, C.scope) self.assertEquals(len(C.stinfo['relations']), 1) + + def test_is_rel_no_scope_2(self): rqlst = self.parse('Any X, ET WHERE C is ET, EXISTS(X work_for C)').children[0] C = rqlst.defined_vars['C'] self.failUnless(C.scope is rqlst, C.scope) self.assertEquals(len(C.stinfo['relations']), 2) - def test_subquery_annotation(self): + + def test_not_rel_normalization_1(self): + rqlst = self.parse('Any X WHERE C is Company, NOT X work_for C').children[0] + self.assertEquals(rqlst.as_string(), 'Any X WHERE C is Company, NOT EXISTS(X work_for C)') + C = rqlst.defined_vars['C'] + self.failIf(C.scope is rqlst, C.scope) + + def test_not_rel_normalization_2(self): + rqlst = self.parse('Any X, ET WHERE C is ET, NOT X work_for C').children[0] + self.assertEquals(rqlst.as_string(), 'Any X,ET WHERE C is ET, NOT EXISTS(X work_for C)') + C = rqlst.defined_vars['C'] + self.failUnless(C.scope is rqlst, C.scope) + + def test_not_rel_normalization_3(self): + rqlst = self.parse('Any X WHERE C is Company, X work_for C, NOT C name "World Company"').children[0] + self.assertEquals(rqlst.as_string(), "Any X WHERE C is Company, X work_for C, NOT C name 'World Company'") + C = rqlst.defined_vars['C'] + self.failUnless(C.scope is rqlst, C.scope) + + def test_not_rel_normalization_4(self): + rqlst = self.parse('Any X WHERE C is Company, NOT (X work_for C, C name "World Company")').children[0] + self.assertEquals(rqlst.as_string(), "Any X WHERE C is Company, NOT EXISTS(X work_for C, C name 'World Company')") + C = rqlst.defined_vars['C'] + self.failIf(C.scope is rqlst, C.scope) + + def test_not_rel_normalization_5(self): + rqlst = self.parse('Any X WHERE X work_for C, EXISTS(C identity D, NOT Y work_for D, D name "World Company")').children[0] + self.assertEquals(rqlst.as_string(), "Any X WHERE X work_for C, EXISTS(C identity D, NOT EXISTS(Y work_for D), D name 'World Company')") + D = rqlst.defined_vars['D'] + self.failIf(D.scope is rqlst, D.scope) + self.failUnless(D.scope.parent.scope is rqlst, D.scope.parent.scope) + + def test_subquery_annotation_1(self): rqlst = self.parse('Any X WITH X BEING (Any X WHERE C is Company, EXISTS(X work_for C))').children[0] C = rqlst.with_[0].query.children[0].defined_vars['C'] self.failIf(C.scope is rqlst, C.scope) self.assertEquals(len(C.stinfo['relations']), 1) + + def test_subquery_annotation_2(self): rqlst = self.parse('Any X,ET WITH X,ET BEING (Any X, ET WHERE C is ET, EXISTS(X work_for C))').children[0] C = rqlst.with_[0].query.children[0].defined_vars['C'] self.failUnless(C.scope is rqlst.with_[0].query.children[0], C.scope)