test/unittest_parser.py
author Philippe Pepiot <philippe.pepiot@logilab.fr>
Thu, 13 Dec 2018 09:32:27 +0100
changeset 819 148f94dda768
parent 809 3f6fd47874d8
child 831 da6c6671ce48
permissions -rw-r--r--
[pkg] version 0.35.0

# -*- coding: iso-8859-1 -*-
# copyright 2004-2016 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/>.
from __future__ import print_function

from six import text_type, PY2

from yapps.runtime import print_error, SyntaxError

from rql.parser import Hercule, HerculeScanner
from rql import BadRQLQuery, RQLSyntaxError, nodes, stmts, parse
from rql import parse

if PY2:
    import unittest2 as unittest
else:
    import unittest


BAD_SYNTAX_QUERIES = (
    'ANY X WHERE X name Nulll;',
    'ANY X WHERE (X name NULL or X name "chouette";',
    'INSERT Person X : X name "bidule" or X name "chouette";',
    'Any X WHERE "YO" related "UPI";',
    # FIXME: incorrect because X/Y are not bound, not a syntax error
#    'SET X travaille Y;',
    "Personne P WHERE OFFSET 200;",

    'Any X GROUPBY X ORDERBY X WHERE X nom "toto" UNION Any X GROUPBY X ORDERBY X WHERE X firstname "toto";',
    '(Any X GROUPBY X WHERE X nom "toto") UNION (Any X GROUPBY X WHERE X firstname "toto") ORDERBY X;',

    'Any X, X/Y FROM (Any SUM(X) WHERE X is Person) WHERE X is Person;', # missing AS for subquery

    'Any X, X/Y FROM (Any X WHERE X is) WHERE X is Person;', # missing AS for subquery

    )

BAD_QUERIES = (
    'Person Marcou;',
    'INSERT Any X : X name "bidule";',
    'DELETE Any X;',
    'Person Marcou',
    'INSERT Person X : Y name "bidule" WHERE X work_for Y;',
    'Any X LIMIT -1;',
    'Any X OFFSET -1;',
    'Any X ORDERBY Y;',
    'Any N,COUNT(X) GROUPBY N '
    ' HAVING COUNT(X)>1 '
    ' WITH X,N BEING (Any X, N WHERE X name N, X is State UNION '
    '                 Any X, N WHERE X name N, X is Transition);',

    'Machin 12',
    )

# FIXME: this shoud be generated from the spec file
SPEC_QUERIES = (
    'Any X WHERE X eid 53;',
    'Any X WHERE X eid -53;',
    "Document X WHERE X occurence_of F, F class C, C name 'Bande dessin�e', X owned_by U, U login 'syt', X available true;",
    u"Document X WHERE X occurence_of F, F class C, C name 'Bande dessin�e', X owned_by U, U login 'syt', X available true;",
    "Personne P WHERE P travaille_pour S, S nom 'Eurocopter', P interesse_par T, T nom 'formation';",
    "Note N WHERE N ecrit_le D, D day > (today -10), N ecrit_par P, P nom 'jphc' or P nom 'ocy';",
    "Personne P WHERE (P interesse_par T, T nom 'formation') or (P ville 'Paris');",
    "Any X ORDERBY S DESC WHERE X is Person, X firstname 'Anne', X surname S;",
    # limit / offset
    "Personne P LIMIT 100 WHERE P nom N;",
    "Personne P LIMIT 100 OFFSET 200 WHERE P nom N;",
    'Any X OFFSET 6 WHERE X is Person;',
    # optional relation support (left|right outer join)
    'Any X,Y,A WHERE X? concerns Y, Y title A;',
    'Any X,Y,A WHERE X concerns Y?, Y title A;',
    'Any X,Y,A WHERE X? concerns Y?, Y title A;',
    # data modification query
    "INSERT Personne X: X nom 'bidule';",
    "INSERT Personne X, Personne Y: X nom 'bidule', Y nom 'chouette', X ami Y;",
    "INSERT Person X: X nom 'bidule', X ami Y WHERE Y nom 'chouette';",
    "SET X nom 'toto', X prenom 'original' WHERE X is Person, X nom 'bidule';",
    "SET X know Y WHERE X ami Y;",
    "SET X value -Y WHERE X value Y;",
    "DELETE Person X WHERE X nom 'toto';",
    "DELETE X ami Y WHERE X is Person, X nom 'toto';",

    # some additional cases
    'INSERT Person X : X name "bidule", Y workfor X WHERE Y name "logilab";',
    'DISTINCT Any X,A,B,C,D ORDERBY A ASC WHERE P eid 41, X concerns P, P is Project, X is Story,X title A,X state B,X priority C,X cost D;',
    'Any X WHERE X has_text "2.12.0";',
    'Any X,A,B,C,D ORDERBY A ASC WHERE X concerns 41,X title A,X state B,X priority C,X cost D;',

    "Any X, COUNT(B) GROUPBY X ORDERBY 1 where B concerns X;",

    "Any X ORDERBY RANDOM();",
    "Any X ORDERBY F(1, 2);",

    "Any X, COUNT(B) GROUPBY X ORDERBY 1 WHERE B concerns X HAVING COUNT(B) > 2;",

    'Any X, MAX(COUNT(B)) GROUPBY X WHERE B concerns X;', # syntaxically correct

    'Any X WHERE X eid > 12;',
    'DELETE Any X WHERE X eid > 12;',

#    'Any X WHERE 5 in_state X;',
    '(Any X WHERE X eid > 12) UNION (Any X WHERE X eid < 23);',
    '(Any X WHERE X nom "toto") UNION (Any X WHERE X firstname "toto");',
    '(Any X GROUPBY X WHERE X nom "toto") UNION (Any X GROUPBY X ORDERBY X WHERE X firstname "toto");',

    'Any X, X/Y WHERE X is Person WITH Y BEING (Any SUM(X) WHERE X is Person);',
    'Any Y, COUNT(X) GROUPBY Y WHERE X bla Y WITH Y BEING ((Person X) UNION (Document X));',

    'Any T2, COUNT(T1)'
    ' GROUPBY T1'
    ' ORDERBY 2 DESC, T2;'
    ' WITH T1,T2 BEING ('
    '      (Any X,N WHERE X name N, X transition_of E, E name %(name)s)'
    '       UNION '
    '      (Any X,N WHERE X name N, X state_of E, E name %(name)s));',


    'Any T2'
    ' GROUPBY T2'
    ' WHERE T1 relation T2'
    ' HAVING COUNT(T1) IN (1,2);',

    'Any T2'
    ' GROUPBY T2'
    ' WHERE T1 relation T2'
    ' HAVING COUNT(T1) IN (1,2) OR COUNT(T1) IN (3,4);',

    'Any T2'
    ' GROUPBY T2'
    ' WHERE T1 relation T2'
    ' HAVING 1 < COUNT(T1) OR COUNT(T1) IN (3,4);',

    'Any T2'
    ' GROUPBY T2'
    ' WHERE T1 relation T2'
    ' HAVING (COUNT(T1) IN (1,2)) OR (COUNT(T1) IN (3,4));',

    'Any T2'
    ' GROUPBY T2'
    ' WHERE T1 relation T2'
    ' HAVING (1 < COUNT(T1) OR COUNT(T1) IN (3,4));',

    'Any T2'
    ' GROUPBY T2'
    ' WHERE T1 relation T2'
    ' HAVING 1+2 < COUNT(T1);',

    'Any X,Y,A ORDERBY Y '
    'WHERE A done_for Y, X split_into Y, A diem D '
    'HAVING MIN(D) < "2010-07-01", MAX(D) >= "2010-07-01";',

    'Any YEAR(XD),COUNT(X) GROUPBY YEAR(XD) ORDERBY YEAR(XD) WHERE X date XD;',
    'Any YEAR(XD),COUNT(X) GROUPBY 1 ORDERBY 1 WHERE X date XD;',

    'Any -1.0;',

    'Any U,G WHERE U login UL, G name GL, G is CWGroup HAVING UPPER(UL)=UPPER(GL)?;',

    'Any U,G WHERE U login UL, G name GL, G is CWGroup HAVING UPPER(UL)?=UPPER(GL);',
    )

class ParserHercule(unittest.TestCase):
    _syntaxerr = SyntaxError

    def parse(self, string, print_errors=False):
        try:
            parser = Hercule(HerculeScanner(string))
            return parser.goal()
        except SyntaxError as ex:
            if print_errors:
                # try to get error message from yapps
                print_error(ex, parser._scanner)
                print
            raise
        except Exception as ex:
            if print_errors:
                print(string, ex)
            raise

    def test_unicode_constant(self):
        tree = self.parse(u"Any X WHERE X name '�ngstr�m';")
        base = tree.children[0].where
        comparison = base.children[1]
        self.assertIsInstance(comparison, nodes.Comparison)
        rhs = comparison.children[0]
        self.assertIsInstance(rhs.value, text_type)

    def test_precedence_1(self):
        tree = self.parse("Any X WHERE X firstname 'lulu' AND X name 'toto' OR X name 'tutu';")
        base = tree.children[0].where
        self.assertEqual(isinstance(base, nodes.Or), 1)
        self.assertEqual(isinstance(base.children[0], nodes.And), 1)
        self.assertEqual(isinstance(base.children[1], nodes.Relation), 1)
        self.assertEqual(str(tree), 'Any X WHERE (X firstname "lulu", X name "toto") OR (X name "tutu")')

    def test_precedence_2(self):
        tree = self.parse("Any X WHERE X firstname 'lulu', X name 'toto' OR X name 'tutu';")
        base = tree.children[0].where
        self.assertEqual(isinstance(base, nodes.And), 1)
        self.assertEqual(isinstance(base.children[0], nodes.Relation), 1)
        self.assertEqual(isinstance(base.children[1], nodes.Or), 1)
        self.assertEqual(str(tree), 'Any X WHERE X firstname "lulu", (X name "toto") OR (X name "tutu")')

    def test_precedence_3(self):
        tree = self.parse("Any X WHERE X firstname 'lulu' AND (X name 'toto' or X name 'tutu');")
        base = tree.children[0].where
        self.assertEqual(isinstance(base, nodes.And), 1)
        self.assertEqual(isinstance(base.children[0], nodes.Relation), 1)
        self.assertEqual(isinstance(base.children[1], nodes.Or), 1)
        self.assertEqual(str(tree), 'Any X WHERE X firstname "lulu", (X name "toto") OR (X name "tutu")')

    def test_precedence_4(self):
        tree = self.parse("Any X WHERE X firstname 'lulu' OR X name 'toto' AND X name 'tutu';")
        base = tree.children[0].where
        self.assertEqual(isinstance(base, nodes.Or), 1)
        self.assertEqual(isinstance(base.children[0], nodes.Relation), 1)
        self.assertEqual(isinstance(base.children[1], nodes.And), 1)

    def test_not_precedence_0(self):
        tree = self.parse("Any X WHERE NOT X firstname 'lulu', X name 'toto';")
        self.assertEqual(str(tree), 'Any X WHERE NOT X firstname "lulu", X name "toto"')

    def test_not_precedence_1(self):
        tree = self.parse("Any X WHERE NOT X firstname 'lulu' AND X name 'toto';")
        self.assertEqual(str(tree), 'Any X WHERE NOT X firstname "lulu", X name "toto"')

    def test_not_precedence_2(self):
        tree = self.parse("Any X WHERE NOT X firstname 'lulu' OR X name 'toto';")
        self.assertEqual(str(tree), 'Any X WHERE (NOT X firstname "lulu") OR (X name "toto")')

    def test_string_1(self):
        tree = self.parse(r"Any X WHERE X firstname 'lu\"lu';")
        const = tree.children[0].where.children[1].children[0]
        self.assertEqual(const.value, r'lu\"lu')

    def test_string_2(self):
        tree = self.parse(r"Any X WHERE X firstname 'lu\'lu';")
        const = tree.children[0].where.children[1].children[0]
        self.assertEqual(const.value, 'lu\'lu')

    def test_string_3(self):
        tree = self.parse(r'Any X WHERE X firstname "lu\'lu";')
        const = tree.children[0].where.children[1].children[0]
        self.assertEqual(const.value, r"lu\'lu")

    def test_string_4(self):
        tree = self.parse(r'Any X WHERE X firstname "lu\"lu";')
        const = tree.children[0].where.children[1].children[0]
        self.assertEqual(const.value, "lu\"lu")

    def test_math_1(self):
        tree = self.parse(r'Any X WHERE X date (TODAY + 1);')
        math = tree.children[0].where.children[1].children[0]
        self.assertIsInstance(math, nodes.MathExpression)
        self.assertEqual(math.operator, '+')

    def test_math_2(self):
        tree = self.parse(r'Any X WHERE X date (TODAY + 1 * 2);')
        math = tree.children[0].where.children[1].children[0]
        self.assertIsInstance(math, nodes.MathExpression)
        self.assertEqual(math.operator, '+')
        math2 = math.children[1]
        self.assertIsInstance(math2, nodes.MathExpression)
        self.assertEqual(math2.operator, '*')

    def test_math_3(self):
        tree = self.parse(r'Any X WHERE X date (TODAY + 1) * 2;')
        math = tree.children[0].where.children[1].children[0]
        self.assertIsInstance(math, nodes.MathExpression)
        self.assertEqual(math.operator, '*')
        math2 = math.children[0]
        self.assertIsInstance(math2, nodes.MathExpression)
        self.assertEqual(math2.operator, '+')

    def test_substitute(self):
        tree = self.parse("Any X WHERE X firstname %(firstname)s;")
        cste = tree.children[0].where.children[1].children[0]
        self.assertIsInstance(cste, nodes.Constant)
        self.assertEqual(cste.type, 'Substitute')
        self.assertEqual(cste.value, 'firstname')

    def test_optional_relation(self):
        tree = self.parse(r'Any X WHERE X related Y;')
        related = tree.children[0].where
        self.assertEqual(related.optional, None)
        tree = self.parse(r'Any X WHERE X? related Y;')
        related = tree.children[0].where
        self.assertEqual(related.optional, 'left')
        tree = self.parse(r'Any X WHERE X related Y?;')
        related = tree.children[0].where
        self.assertEqual(related.optional, 'right')
        tree = self.parse(r'Any X WHERE X? related Y?;')
        related = tree.children[0].where
        self.assertEqual(related.optional, 'both')

    def test_exists(self):
        tree = self.parse("Any X WHERE X firstname 'lulu',"
                          "EXISTS (X owned_by U, U in_group G, G name 'lulufanclub' OR G name 'managers');")
        self.assertEqual(tree.as_string(),
                         'Any X WHERE X firstname "lulu", '
                         'EXISTS(X owned_by U, U in_group G, (G name "lulufanclub") OR (G name "managers"))')
        exists = tree.children[0].where.get_nodes(nodes.Exists)[0]
        self.assertTrue(exists.children[0].parent is exists)
        self.assertTrue(exists.parent)

    def test_etype(self):
        tree = self.parse('EmailAddress X;')
        self.assertEqual(tree.as_string(), 'Any X WHERE X is EmailAddress')
        tree = self.parse('EUser X;')
        self.assertEqual(tree.as_string(), 'Any X WHERE X is EUser')

    def test_spec(self):
        """test all RQL string found in the specification and test they are well parsed"""
        for rql in SPEC_QUERIES:
#            print("Orig:", rql)
#            print("Resu:", rqltree)
            with self.subTest(rql=rql):
                self.parse(rql, True)

    def test_raise_badsyntax_error(self):
        for rql in BAD_SYNTAX_QUERIES:
            with self.subTest(rql=rql):
                self.assertRaises(self._syntaxerr, self.parse, rql)

    def test_raise_badrqlquery(self):
        BAD_QUERIES = ('Person Marcou;',)
        for rql in BAD_QUERIES:
            with self.subTest(rql=rql):
                self.assertRaises(BadRQLQuery, self.parse, rql)


class ParserRQLHelper(ParserHercule):
    _syntaxerr = RQLSyntaxError

    def parse(self, string, print_errors=False):
        try:
            return parse(string, print_errors)
        except:
            raise


if __name__ == '__main__':
    unittest.main()