Commit 71aceee6 authored by Nsukami Patrick's avatar Nsukami Patrick
Browse files

feat: Remove comparison feature

See: cubicweb#29
parent f9186afc380d
Pipeline #72505 failed with stages
in 1 minute and 32 seconds
......@@ -209,15 +209,6 @@ class RQLHelper:
if rewritten and select.solutions:
select.clean_solutions()
def compare(self, rqlstring1, rqlstring2):
"""Compare 2 RQL requests.
Return True if both requests would return the same results.
"""
from rql.compare import compare_tree
return compare_tree(self.parse(rqlstring1), self.parse(rqlstring2))
def copy_uid_node(select, node, vconsts):
node = node.copy(select)
......
# copyright 2004-2021 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of rql.
#
# rql is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# rql is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with rql. If not, see <http://www.gnu.org/licenses/>.
"""Comparing syntax trees.
"""
__docformat__ = "restructuredtext en"
from rql.nodes import VariableRef, Variable, Function, Relation, Comparison
def compare_tree(request1, request2):
"""Compares 2 RQL requests.
:rtype: bool
:return: True if both requests would return the same results.
"""
return make_canon_dict(request1) == make_canon_dict(request2)
def make_canon_dict(rql_tree, verbose=0):
"""Return a canonical representation of the request as a dictionnary."""
allvars = {}
canon = {
"all_variables": allvars,
"selected": [],
"restriction": {},
}
canon = RQLCanonizer().visit(rql_tree, canon)
# forge variable name
for var, name_parts in allvars.values():
name_parts.sort()
var.name = ":".join(name_parts)
sort(canon)
if verbose:
print("CANON FOR", rql_tree)
from pprint import pprint
pprint(canon)
return canon
def sort(canon_dict):
"""Remove the all_variables entry and sort other entries in place."""
del canon_dict["all_variables"]
canon_dict["selection"].sort()
for values in canon_dict["restriction"].values():
values.sort()
class SkipChildren(Exception):
"""Signal indicating to ignore the current child."""
class RQLCanonizer:
"""Build a dictionnary which represents a RQL syntax tree."""
def visit(self, node, canon):
try:
node.accept(self, canon)
except SkipChildren:
return canon
for c in node.children:
self.visit(c, canon)
return canon
def visit_select(self, select, canon):
allvars = canon["all_variables"]
for var in select.defined_vars.values():
allvars[var] = (Variable(var.name), [])
canon["selection"] = []
selected = select.selected
for i in range(len(selected)):
node = selected[i]
if isinstance(node, VariableRef):
node = node.variable
allvars[node][1].append(str(i))
canon["selection"].append(allvars[node][0])
else: # Function
canon["selection"].append(node)
for var in node.iget_nodes(VariableRef):
var.parent.replace(var, allvars[var.variable][0])
def visit_group(self, group, canon):
canon["group"] = group
def visit_sort(self, sort, canon):
canon["sort"] = sort
def visit_sortterm(self, sortterm, canon):
pass
def visit_and(self, et, canon):
pass
def visit_or(self, ou, canon):
canon_dict = {}
keys = []
for expr in ou.get_nodes(Relation):
key = "%s%s" % (expr.r_type, expr._not)
canon_dict.setdefault(key, []).append(expr)
keys.append(key)
keys.sort()
r_type = ":".join(keys)
r_list = canon["restriction"].setdefault(r_type, [])
done = {}
for key in keys:
if key in done:
continue
done[key] = None
for expr in canon_dict[key]:
self.manage_relation(expr, canon, r_list)
raise SkipChildren()
def manage_relation(self, relation, canon, r_list):
lhs, rhs = relation.get_parts()
# handle special case of the IN function
func = rhs.children[0]
if isinstance(func, Function) and func.name == "IN":
if not relation._not:
base_key = "%s%s" % (relation.r_type, relation._not)
if not canon["restriction"][base_key]:
del canon["restriction"][base_key]
key = ":".join([base_key] * len(func.children))
r_list = canon["restriction"].setdefault(key, [])
for e in func.children:
eq_expr = Relation(relation.r_type, relation._not)
eq_expr.append(lhs)
eq_expr.append(Comparison("=", e))
self.manage_relation(eq_expr, canon, r_list)
# restaure parent attribute to avoid problem later
e.parent = func
lhs.parent = relation
return
# build a canonical representation for this relation
lhs_expr_reminder = make_lhs_reminder(lhs, canon)
rhs_expr_reminder = make_rhs_reminder(rhs, canon)
reminder = (lhs_expr_reminder, rhs_expr_reminder)
# avoid duplicate
if reminder in r_list:
return
r_list.append(reminder)
# make a string which represents this relation (we'll use it later
# to build variables' name)
expr_reminder = relation.r_type
lhs_vars = lhs.get_nodes(VariableRef)
if not lhs_vars:
expr_reminder = "%s_%s" % (lhs, expr_reminder)
rhs_vars = rhs.get_nodes(VariableRef)
if not rhs_vars:
expr_reminder = "%s_%s" % (expr_reminder, rhs)
for var in lhs_vars + rhs_vars:
var = var.variable
canon["all_variables"][var][1].append(expr_reminder)
def visit_relation(self, relation, canon):
key = "%s%s" % (relation.r_type, relation._not)
r_list = canon["restriction"].setdefault(key, [])
self.manage_relation(relation, canon, r_list)
def visit_comparison(self, comparison, canon):
"""do nothing for this node type"""
def visit_mathexpression(self, mathexpression, canon):
"""do nothing for this node type"""
def visit_function(self, function, canon):
"""do nothing for this node type"""
def visit_variableref(self, varref, canon):
varref.parent.replace(varref, canon["all_variables"][varref.variable][0])
def visit_constant(self, constante, canon):
"""do nothing for this node type"""
def visit_union(self, *args):
raise NotImplementedError("union comparison not implemented")
def make_lhs_reminder(lhs, canon):
"""Return a reminder for a relation's left hand side
(i.e a VariableRef object).
"""
try:
lhs = canon["all_variables"][lhs.variable][0]
except (KeyError, IndexError):
pass
return ("=", lhs)
def make_rhs_reminder(rhs, canon):
"""Return a reminder for a relation's right hand side
(i.e a Comparison object).
"""
child = rhs.children[0]
try:
child = canon["all_variables"][child.variable][0]
except AttributeError:
pass
return (rhs.operator, child)
# copyright 2004-2021 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of rql.
#
# rql is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# rql is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with rql. If not, see <http://www.gnu.org/licenses/>.
import unittest
from logilab.common.testlib import TestCase, SkipTest
from rql import RQLHelper
from unittest_analyze import RelationSchema, EntitySchema, DummySchema as BaseSchema
class DummySchema(BaseSchema):
def __init__(self):
super(DummySchema, self).__init__()
for etype in [
"Note",
]:
self._types[etype] = EntitySchema(etype)
relations = [
("a_faire_par", (("Note", ("Person",)),)),
("creation_date", (("Note", ("Date",)),)),
]
for rel_name, rel_ent in relations:
self._relations[rel_name] = RelationSchema(rel_ent)
self._relations["nom"] = self._relations["name"]
self._relations["prenom"] = self._relations["firstname"]
class RQLCompareClassTest(TestCase):
"""Compare RQL strings"""
@classmethod
def setUpClass(cls):
raise SkipTest("broken")
def setUp(self):
self.h = RQLHelper(DummySchema(), None)
def _compareEquivalent(self, r1, r2):
"""fails if the RQL strings r1 and r2 are equivalent"""
self.skipTest("broken")
self.failUnless(self.h.compare(r1, r2), "r1: %s\nr2: %s" % (r1, r2))
def _compareNotEquivalent(self, r1, r2):
"""fails if the RQL strings r1 and r2 are not equivalent"""
self.failIf(self.h.compare(r1, r2), "r1: %s\nr2: %s" % (r1, r2))
# equivalent queries ##################################################
def test_same_request_simple(self):
r = "Any X WHERE X is Note ;"
self._compareEquivalent(r, r)
def test_same_request_diff_names(self):
r1 = "Any X ;"
r2 = "Any Y ;"
self._compareEquivalent(r1, r2)
def test_same_request_diff_names_simple(self):
r1 = "Any X WHERE X is Note ;"
r2 = "Any Y WHERE Y is Note ;"
self._compareEquivalent(r1, r2)
def test_same_request_any(self):
r1 = "Any X WHERE X is Note ;"
r2 = "Note X ;"
self._compareEquivalent(r1, r2)
def test_same_request_any_diff_names(self):
r1 = "Any X WHERE X is Note ;"
r2 = "Note Y ;"
self._compareEquivalent(r1, r2)
def test_same_request_complex(self):
r = (
"Any N, N2 WHERE N is Note, N2 is Note, N a_faire_par P1, "
"P1 nom 'jphc', N2 a_faire_par P2, P2 nom 'ocy' ;"
)
self._compareEquivalent(r, r)
def test_same_request_comma_and(self):
r1 = (
"Any N, N2 WHERE N is Note, N2 is Note, N a_faire_par P1, "
"P1 nom 'jphc', N2 a_faire_par P2, P2 nom 'ocy' ;"
)
r2 = (
"Any N, N2 WHERE N is Note AND N2 is Note AND N a_faire_par P1 "
"AND P1 nom 'jphc' AND N2 a_faire_par P2 AND P2 nom 'ocy' ;"
)
self._compareEquivalent(r1, r2)
def test_same_request_diff_names_complex(self):
r1 = (
"Any N, N2 WHERE N is Note, N2 is Note, N a_faire_par P1, "
"P1 nom 'jphc', N2 a_faire_par P2, P2 nom 'ocy' ;"
)
r2 = (
"Any Y, X WHERE X is Note, Y is Note, X a_faire_par A1, "
"A1 nom 'ocy', Y a_faire_par A2, A2 nom 'jphc' ;"
)
self._compareEquivalent(r1, r2)
def test_same_request_diff_order(self):
r1 = (
"Any N, N2 WHERE N is Note, N2 is Note, N a_faire_par P1, "
"P1 nom 'jphc', N2 a_faire_par P2, P2 nom 'ocy' ;"
)
r2 = (
"Any N, N2 WHERE N2 is Note, N is Note, N a_faire_par P1, "
"N2 a_faire_par P2, P2 nom 'ocy', P1 nom 'jphc' ;"
)
self._compareEquivalent(r1, r2)
def test_same_request_diff_order_diff_names(self):
r1 = (
"Any N, N2 WHERE N is Note, N2 is Note, N a_faire_par P1, "
"P1 nom 'jphc', N2 a_faire_par P2, P2 nom 'ocy' ;"
)
r2 = (
"Any Y, X WHERE X is Note, X a_faire_par P1, P1 nom 'ocy', "
"Y is Note, Y a_faire_par P2, P2 nom 'jphc' ;"
)
self._compareEquivalent(r1, r2)
def test_same_request_with_comparison(self):
r1 = "Note N WHERE N a_faire_par P, P nom 'jphc', K creation_date > today-10;"
r2 = "Note K WHERE K a_faire_par Y, K creation_date > today-10, Y nom 'jphc';"
self._compareEquivalent(r1, r2)
def test_same_request_in_or(self):
r1 = (
"Note N WHERE N creation_date > today-10, N a_faire_par P, "
"P nom 'jphc' or P nom 'ludal';"
)
r2 = "Note K WHERE K a_faire_par Y, K creation_date > today-10, Y nom in ('jphc', 'ludal');"
self._compareEquivalent(r1, r2)
def test_same_request_reverse_or(self):
r1 = (
"Note N WHERE N creation_date > today-10, N a_faire_par P, "
"P nom 'jphc' or P nom 'ludal';"
)
r2 = (
"Note N WHERE N creation_date > today-10, N a_faire_par P, "
"P nom 'ludal' or P nom 'jphc';"
)
self._compareEquivalent(r1, r2)
def test_same_request_reverse_or2(self):
r1 = (
"Note N WHERE N creation_date > today-10, N a_faire_par P, "
"P prenom 'jphc' or P nom 'ludal';"
)
r2 = (
"Note N WHERE N creation_date > today-10, N a_faire_par P, "
"P nom 'ludal' or P prenom 'jphc';"
)
self._compareEquivalent(r1, r2)
def test_same_request_duplicate_expr(self):
r1 = "Note N WHERE N creation_date D, N creation_date D;"
r2 = "Note N WHERE N creation_date D;"
self._compareEquivalent(r1, r2)
def test_same_request_not_in_or(self):
r1 = (
"Note K WHERE K a_faire_par Y, K creation_date > today-10, "
"not Y nom in ('jphc', 'ludal');"
)
r2 = (
"Note K WHERE K a_faire_par Y, K creation_date > today-10, "
"not Y nom 'jphc' and not Y nom 'ludal';"
)
self._compareEquivalent(r1, r2)
# non equivalent queries ##################################################
def test_diff_request(self):
r1 = (
"Any N, N2 WHERE N is Note, N2 is Note, N a_faire_par P1, "
"P1 nom 'jphc', N2 a_faire_par P2, P2 nom 'ocy' ;"
)
r2 = "Any X WHERE X is Note ;"
self._compareNotEquivalent(r1, r2)
def test_diff_request_and_or(self):
r1 = (
"Note N WHERE N creation_date > today-10, N a_faire_par P, "
"P nom 'jphc' or P nom 'ludal';"
)
r2 = (
"Note N WHERE N creation_date > today-10, N a_faire_par P, "
"P nom 'jphc', P nom 'ludal';"
)
self._compareNotEquivalent(r1, r2)
def test_diff_request_and_or2(self):
r1 = (
"Note N WHERE N creation_date > today-10, N a_faire_par P, "
"P nom 'jphc' or P prenom 'ludal';"
)
r2 = (
"Note N WHERE N creation_date > today-10, N a_faire_par P, "
"P nom 'jphc', P prenom 'ludal';"
)
self._compareNotEquivalent(r1, r2)
def test_diff_request_non_selected_var(self):
r1 = "Any X, D WHERE X is Note, X creation_date D ;"
r2 = "Any X WHERE X is Note, X creation_date D ;"
self._compareNotEquivalent(r1, r2)
def test_diff_request_aggregat(self):
r1 = "Any X, D WHERE X is Note, X creation_date D ;"
r2 = "Any X, MAX(D) WHERE X is Note, X creation_date D ;"
self._compareNotEquivalent(r1, r2)
def test_diff_request_group(self):
r1 = "Any X GROUPBY X WHERE X is Note;"
r2 = "Any X WHERE X is Note;"
self._compareNotEquivalent(r1, r2)
def test_diff_request_sort(self):
r1 = "Any X ORDERBY X WHERE X is Note;"
r2 = "Any X WHERE X is Note;"
self._compareNotEquivalent(r1, r2)
def test_diff_request_not(self):
r1 = "Any X WHERE NOT X is Note ;"
r2 = "Any X WHERE X is Note;"
self._compareNotEquivalent(r1, r2)
def test_diff_request_not_in_or(self):
r1 = (
"Note K WHERE K a_faire_par Y, K creation_date > today-10, "
"not Y nom in ('jphc', 'ludal');"
)
r2 = (
"Note K WHERE K a_faire_par Y, K creation_date > today-10, "
"not Y nom 'jphc' or not Y nom 'ludal';"
)
self._compareNotEquivalent(r1, r2)
if __name__ == "__main__":
unittest.main()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment