diff --git a/requirements-dev.txt b/requirements-dev.txt index 553cefb..2cf708b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ -r requirements-test.txt pip-tools==6.6.0 -setuptools==65.5.1 +setuptools==70.0.0 wheel==0.38.1 pylint==2.13.5 pycodestyle==2.8.0 diff --git a/src/cymple/builder.py b/src/cymple/builder.py index a5042eb..bcc993e 100644 --- a/src/cymple/builder.py +++ b/src/cymple/builder.py @@ -3,7 +3,7 @@ # pylint: disable=R0901 # pylint: disable=R0903 # pylint: disable=W0102 -from typing import List, Union +from typing import List, Union, Dict, Any from .typedefs import Mapping, Properties @@ -52,6 +52,41 @@ def call(self): return CallAvailable(self.query + ' CALL') +class Case(Query): + """A class for representing a "CASE" clause.""" + + def case(self, when_then_mapping: Dict[str, Union[List[str], str]], default_result: str, results_ref: str = None, test_expression: str = None): + """Concatenate a CASE clause to the query, to compare a single expression against multiple values or to express multiple conditional statements. + + :param when_then_mapping: A dictionary such that a value represents a literal expression (or a list of + expressions) whose result will be compared to test_expression if given, else is evaluated to a BOOLEAN, and it's key represents the literal expression returned as output if the value matches test_expression if giver, or if the expression evaluates to TRUE. + :type when_then_mapping: Dict[str, Union[List[str], str]] + :param default_result: The expression to return if no value matches the test expression if given, or if all + expressions evaluated as FALSE. + :type default_result: str + :param results_ref: The reference name of the resulted returned values, plus any other desired reference names + to return., defaults to None + :type results_ref: str + :param test_expression: An expression to test the cases against. For example, 'n.name'. Optional., defaults to + None + :type test_expression: str + + :return: A Query object with a query that contains the new clause. + :rtype: CaseAvailable + """ + ret = " CASE" + if test_expression is not None: + ret += f" {test_expression}" + for then, when in when_then_mapping.items(): + if type(when) is not list: + when = [when] + ret += f" WHEN {', '.join(when)} THEN {then}" + ret += f" ELSE {default_result} END" + if results_ref is not None: + ret += f" AS {results_ref}" + return CaseAvailable(self.query + ret) + + class CaseWhen(Query): """A class for representing a "CASE WHEN" clause.""" @@ -78,7 +113,7 @@ def case_when(self, filters: dict, on_true: str, on_false: str, ref_name: str, c :rtype: CaseWhenAvailable """ filt = ' CASE WHEN ' + Properties(filters).to_str(comparison_operator, boolean_operator, **kwargs) - filt += f' THEN {on_true} ELSE {on_false} END as {ref_name}' + filt += f' THEN {on_true} ELSE {on_false} END AS {ref_name}' return CaseWhenAvailable(self.query + filt) @@ -614,17 +649,19 @@ def remove(self, properties: Union[str, List[str]]): class Return(Query): """A class for representing a "RETURN" clause.""" - def return_literal(self, literal: str): + def return_literal(self, literal: str = None): """Concatenate a literal RETURN statement. :param literal: A Cypher string describing the objects to be returned, referencing name/names which were - defined earlier in the query + defined earlier in the query, defaults to None :type literal: str :return: A Query object with a query that contains the new clause. :rtype: ReturnAvailable """ - ret = f' RETURN {literal}' + ret = f' RETURN' + if literal is not None: + ret += f' {literal}' return ReturnAvailable(self.query + ret) @@ -642,7 +679,7 @@ def return_mapping(self, mappings: List[Mapping]): ret = ' RETURN ' + \ ', '.join( - f'{mapping[0]} as {mapping[1]}' if mapping[1] else mapping[0].replace(".", "_") + f'{mapping[0]} AS {mapping[1]}' if mapping[1] else mapping[0].replace(".", "_") for mapping in mappings) return ReturnAvailable(self.query + ret) @@ -840,7 +877,7 @@ def yield_(self, mappings: List[Mapping]): mappings = [mappings] query = ' YIELD ' + \ - ', '.join(f'{mapping[0]} as ' + ', '.join(f'{mapping[0]} AS ' f'{mapping[1] if mapping[1] else mapping[0].replace(".", "_")}' for mapping in mappings) return YieldAvailable(self.query + query) @@ -854,6 +891,10 @@ class CallAvailable(Procedure): """A class decorator declares a Call is available in the current query.""" +class CaseAvailable(QueryStartAvailable, Unwind, Where, Set, Remove, CaseWhen, Return, Limit, Skip, OrderBy, Union): + """A class decorator declares a Case is available in the current query.""" + + class CaseWhenAvailable(QueryStartAvailable, Unwind, Where, CaseWhen, Return, Set): """A class decorator declares a CaseWhen is available in the current query.""" @@ -922,7 +963,7 @@ class RemoveAvailable(Set, Return, Union): """A class decorator declares a Remove is available in the current query.""" -class ReturnAvailable(QueryStartAvailable, Unwind, Return, Limit, Skip, OrderBy, Union): +class ReturnAvailable(QueryStartAvailable, Unwind, Return, Limit, Skip, OrderBy, Union, CaseWhen, Case): """A class decorator declares a Return is available in the current query.""" @@ -950,7 +991,7 @@ class WhereAvailable(Return, Delete, Where, Set, Remove, OperatorStart, QuerySta """A class decorator declares a Where is available in the current query.""" -class WithAvailable(QueryStartAvailable, Unwind, Where, Set, Remove, CaseWhen, Return, Limit, Skip, OrderBy): +class WithAvailable(QueryStartAvailable, Unwind, Where, Set, Remove, CaseWhen, Return, Limit, Skip, OrderBy, Case): """A class decorator declares a With is available in the current query.""" @@ -958,7 +999,7 @@ class YieldAvailable(QueryStartAvailable, Node, Where, Return): """A class decorator declares a Yield is available in the current query.""" -class AnyAvailable(Call, CaseWhen, Create, Delete, Limit, Match, Merge, Node, NodeAfterMerge, OnCreate, OnMatch, OperatorEnd, OperatorStart, OrderBy, Procedure, QueryStart, Relation, RelationAfterMerge, Remove, Return, Set, SetAfterMerge, Skip, Union, Unwind, Where, With, Yield): +class AnyAvailable(Call, Case, CaseWhen, Create, Delete, Limit, Match, Merge, Node, NodeAfterMerge, OnCreate, OnMatch, OperatorEnd, OperatorStart, OrderBy, Procedure, QueryStart, Relation, RelationAfterMerge, Remove, Return, Set, SetAfterMerge, Skip, Union, Unwind, Where, With, Yield): """A class decorator declares anything is available in the current query.""" diff --git a/src/cymple/internal/declarations/case.json b/src/cymple/internal/declarations/case.json new file mode 100644 index 0000000..0539dd4 --- /dev/null +++ b/src/cymple/internal/declarations/case.json @@ -0,0 +1,42 @@ +{ + "clause_name": "CASE", + "methods": [ + { + "name": "case", + "docstring_summary": "Concatenate a CASE clause to the query, to compare a single expression against multiple values or to express multiple conditional statements.", + "args": { + "when_then_mapping": { + "type": "Dict[str, Union[List[str], str]]", + "description": "A dictionary such that a value represents a literal expression (or a list of expressions) whose result will be compared to test_expression if given, else is evaluated to a BOOLEAN, and it's key represents the literal expression returned as output if the value matches test_expression if giver, or if the expression evaluates to TRUE." + }, + "default_result": { + "type": "str", + "description": "The expression to return if no value matches the test expression if given, or if all expressions evaluated as FALSE." + }, + "results_ref": { + "type": "str", + "description": "The reference name of the resulted returned values, plus any other desired reference names to return.", + "default": "None" + }, + "test_expression": { + "type": "str", + "description": "An expression to test the cases against. For example, 'n.name'. Optional.", + "default": "None" + } + } + } + ], + "successors": [ + "QueryStartAvailable", + "Unwind", + "Where", + "Set", + "Remove", + "CaseWhen", + "Return", + "Limit", + "Skip", + "OrderBy", + "Union" + ] +} \ No newline at end of file diff --git a/src/cymple/internal/declarations/return.json b/src/cymple/internal/declarations/return.json index 9225c2f..1d9fafa 100644 --- a/src/cymple/internal/declarations/return.json +++ b/src/cymple/internal/declarations/return.json @@ -7,7 +7,8 @@ "args": { "literal": { "type": "str", - "description": "A Cypher string describing the objects to be returned, referencing name/names which were defined earlier in the query" + "description": "A Cypher string describing the objects to be returned, referencing name/names which were defined earlier in the query", + "default": "None" } } }, @@ -29,6 +30,8 @@ "Limit", "Skip", "OrderBy", - "Union" + "Union", + "CaseWhen", + "Case" ] } \ No newline at end of file diff --git a/src/cymple/internal/declarations/with.json b/src/cymple/internal/declarations/with.json index 1d7e3ce..654d4fd 100644 --- a/src/cymple/internal/declarations/with.json +++ b/src/cymple/internal/declarations/with.json @@ -22,6 +22,7 @@ "Return", "Limit", "Skip", - "OrderBy" + "OrderBy", + "Case" ] } \ No newline at end of file diff --git a/src/cymple/internal/internal_renderer.py b/src/cymple/internal/internal_renderer.py index 40526f3..86e44e2 100644 --- a/src/cymple/internal/internal_renderer.py +++ b/src/cymple/internal/internal_renderer.py @@ -138,7 +138,7 @@ def render_builder_code(): clauses_output += '# pylint: disable=R0901\n' clauses_output += '# pylint: disable=R0903\n' clauses_output += '# pylint: disable=W0102\n' - clauses_output += 'from typing import List, Union\n' + clauses_output += 'from typing import List, Union, Dict, Any\n' clauses_output += 'from .typedefs import Mapping, Properties\n\n' clauses_output += inspect.getsource(query_class) + '\n\n' diff --git a/src/cymple/internal/overloads/case.py b/src/cymple/internal/overloads/case.py index 8b0de00..a0c18ed 100644 --- a/src/cymple/internal/overloads/case.py +++ b/src/cymple/internal/overloads/case.py @@ -1,6 +1,15 @@ - - -def case_when(self, filters: dict, on_true: str, on_false: str, ref_name: str, comparison_operator: str = '"', boolean_operator: str = 'AND', **kwargs): - filt = ' CASE WHEN ' + Properties(filters).to_str(comparison_operator, boolean_operator, **kwargs) - filt += f' THEN {on_true} ELSE {on_false} END as {ref_name}' - return CaseWhenAvailable(self.query + filt) +from typing import Dict, List, Union + + +def case(when_then_mapping: Dict[str, Union[List[str], str]], default_result: str, results_ref: str, test_expression: str = None): + ret = " CASE" + if test_expression is not None: + ret += f" {test_expression}" + for then, when in when_then_mapping.items(): + if type(when) is not list: + when = [when] + ret += f" WHEN {', '.join(when)} THEN {then}" + ret += f" ELSE {default_result} END" + if results_ref is not None: + ret += f" AS {results_ref}" + return CaseAvailable(self.query + ret) diff --git a/src/cymple/internal/overloads/case_when.py b/src/cymple/internal/overloads/case_when.py new file mode 100644 index 0000000..b4f9fcb --- /dev/null +++ b/src/cymple/internal/overloads/case_when.py @@ -0,0 +1,6 @@ + + +def case_when(self, filters: dict, on_true: str, on_false: str, ref_name: str, comparison_operator: str = '"', boolean_operator: str = 'AND', **kwargs): + filt = ' CASE WHEN ' + Properties(filters).to_str(comparison_operator, boolean_operator, **kwargs) + filt += f' THEN {on_true} ELSE {on_false} END AS {ref_name}' + return CaseWhenAvailable(self.query + filt) diff --git a/src/cymple/internal/overloads/returns.py b/src/cymple/internal/overloads/returns.py index 55b33f8..5ddb571 100644 --- a/src/cymple/internal/overloads/returns.py +++ b/src/cymple/internal/overloads/returns.py @@ -1,6 +1,8 @@ -def return_literal(self, literal: str): - ret = f' RETURN {literal}' +def return_literal(self, literal): + ret = f' RETURN' + if literal is not None: + ret += f' {literal}' return ReturnAvailable(self.query + ret) @@ -11,7 +13,7 @@ def return_mapping(self, mappings): ret = ' RETURN ' + \ ', '.join( - f'{mapping[0]} as {mapping[1]}' if mapping[1] else mapping[0].replace(".", "_") + f'{mapping[0]} AS {mapping[1]}' if mapping[1] else mapping[0].replace(".", "_") for mapping in mappings) return ReturnAvailable(self.query + ret) diff --git a/src/cymple/internal/overloads/yield_.py b/src/cymple/internal/overloads/yield_.py index d242936..3c7e9cd 100644 --- a/src/cymple/internal/overloads/yield_.py +++ b/src/cymple/internal/overloads/yield_.py @@ -3,7 +3,7 @@ def yield_(self, mappings): mappings = [mappings] query = ' YIELD ' + \ - ', '.join(f'{mapping[0]} as ' + ', '.join(f'{mapping[0]} AS ' f'{mapping[1] if mapping[1] else mapping[0].replace(".", "_")}' for mapping in mappings) return YieldAvailable(self.query + query) diff --git a/src/cymple/version.py b/src/cymple/version.py index 8dee38b..37d4ad1 100644 --- a/src/cymple/version.py +++ b/src/cymple/version.py @@ -1,3 +1,3 @@ """Version information for Cymple""" -__version__: str = "0.11.0" +__version__: str = "0.12.0" diff --git a/tests/unit/test_clauses.py b/tests/unit/test_clauses.py index 3274c8f..4dcca16 100644 --- a/tests/unit/test_clauses.py +++ b/tests/unit/test_clauses.py @@ -59,13 +59,18 @@ 'UNION': qb.match().node(ref_name='n', labels="Actor").return_mapping([('n.name', 'name')]).union().match().node( ref_name='n', labels="Movie").return_mapping([('n.title', 'name')]), 'UNION (all)': qb.match().node(ref_name='n', labels="Actor").return_mapping( - [('n.name', 'name')]).union_all().match().node(ref_name='n', labels="Movie").return_mapping([('n.title', 'name')]) + [('n.name', 'name')]).union_all().match().node(ref_name='n', labels="Movie").return_mapping([('n.title', 'name')]), + 'CASE (simple)': qb.match().node(ref_name="n", labels="Person").return_literal().case(when_then_mapping={"1": "'blue'", "2": ["'brown'", "'hazel'"]}, default_result="3", results_ref="result, n.eyes", test_expression="n.eyes"), + 'CASE (simple extended)': qb.match().node(ref_name="n", labels="Person").return_literal("n.name,").case(when_then_mapping={"'Unknown'": ["IS NULL", "IS NOT TYPED INTEGER | FLOAT"], "'Baby'": ["= 0", "= 1", "= 2"], "'Child'": "<= 13", "'Teenager'": "< 20", "'Young Adult'": "< 30", "'Immortal'": "> 1000"}, default_result="'Adult'", test_expression="n.age", results_ref="result"), + 'CASE (generic)': qb.match().node(ref_name="n", labels="Person").return_literal().case(when_then_mapping={"1": "n.eyes = 'blue'", "2": "n.age < 40"}, default_result="3", results_ref="result, n.eyes, n.age"), + 'CASE (null values)': qb.match().node(ref_name="n", labels="Person").return_literal("n.name,").case(when_then_mapping={"-1": "n.age IS NULL"}, default_result="n.age - 10", results_ref="age_10_years_ago"), + 'CASE (with set)': qb.match().node(ref_name="n", labels="Person").with_("n,").case(when_then_mapping={"1": "'blue'", "2": "'brown'"}, default_result="3", results_ref="colorCode", test_expression="n.eyes").set({"n.colorCode": "colorCode"}, escape_values=False).return_literal("n.name, n.colorCode") } expected = { '_RESET_': '', 'CALL': 'CALL db.labels()', - 'CASE WHEN': 'MATCH (n) WITH n CASE WHEN n.name = "Bob" THEN true ELSE false END as my_boolean', + 'CASE WHEN': 'MATCH (n) WITH n CASE WHEN n.name = "Bob" THEN true ELSE false END AS my_boolean', 'DELETE': 'MATCH (n) DELETE n', 'DETACH DELETE': 'MATCH (n) DETACH DELETE n', 'WHERE (single)': 'MATCH (n) WHERE n.name = "value"', @@ -85,8 +90,8 @@ 'RELATION (variable length, empty)': 'MATCH ()-[*]-()', 'RELATION (variable length, with label)': 'MATCH ()-[rel: Relation*1..2]-()', 'RETURN (literal)': 'MATCH (n) RETURN n', - 'RETURN (mapping)': 'MATCH (n) RETURN n.name as name', - 'RETURN (mapping, list)': 'MATCH (n) RETURN n.name as name, n.age as age', + 'RETURN (mapping)': 'MATCH (n) RETURN n.name AS name', + 'RETURN (mapping, list)': 'MATCH (n) RETURN n.name AS name, n.age AS age', 'SET': 'MERGE (n) SET n.name = "Alice"', 'SET (not escaping)': 'MERGE (n) SET n.name = n.name + "!"', 'SET (string)': 'MERGE (n) SET n.name = n.name + "!"', @@ -97,8 +102,8 @@ 'UNWIND': 'MATCH (n) WITH n UNWIND n', 'WITH': 'MATCH (a) WITH a,b', 'WITH (start)': 'WITH a MATCH (a) WITH a,b', - 'YIELD': 'CALL db.labels() YIELD label as label WHERE label CONTAINS "User" RETURN count(label) AS numLabels', - 'YIELD (list)': 'CALL db.labels() YIELD length(labels) as len, count(label) as cnt', + 'YIELD': 'CALL db.labels() YIELD label AS label WHERE label CONTAINS "User" RETURN count(label) AS numLabels', + 'YIELD (list)': 'CALL db.labels() YIELD length(labels) AS len, count(label) AS cnt', 'LIMIT': 'MATCH (n) RETURN n LIMIT 1', 'LIMIT (expression)': 'MATCH (n) RETURN n LIMIT 1 + toInteger(3 * rand())', 'LIMIT (with)': 'MATCH (n) WITH n LIMIT 1', @@ -114,8 +119,20 @@ 'CREATE': 'CREATE (n) RETURN n', 'REMOVE': 'MATCH (n) REMOVE n.name RETURN n.age, n.name', 'REMOVE (list)': 'MATCH (n) REMOVE n.age, n.name RETURN n.age, n.name', - 'UNION': 'MATCH (n: Actor) RETURN n.name as name UNION MATCH (n: Movie) RETURN n.title as name', - 'UNION (all)': 'MATCH (n: Actor) RETURN n.name as name UNION ALL MATCH (n: Movie) RETURN n.title as name', + 'UNION': 'MATCH (n: Actor) RETURN n.name AS name UNION MATCH (n: Movie) RETURN n.title AS name', + 'UNION (all)': 'MATCH (n: Actor) RETURN n.name AS name UNION ALL MATCH (n: Movie) RETURN n.title AS name', + 'CASE (simple)': "MATCH (n: Person) RETURN CASE n.eyes WHEN 'blue' THEN 1 WHEN 'brown', 'hazel' THEN 2 ELSE 3 END" + " AS result, n.eyes", + 'CASE (simple extended)': "MATCH (n: Person) RETURN n.name, CASE n.age WHEN IS NULL, IS NOT TYPED INTEGER | FLOAT" + " THEN 'Unknown' WHEN = 0, = 1, = 2 THEN 'Baby' WHEN <= 13 THEN 'Child' WHEN < 20 THEN" + " 'Teenager' WHEN < 30 THEN 'Young Adult' WHEN > 1000 THEN 'Immortal' ELSE 'Adult' END" + " AS result", + 'CASE (generic)': "MATCH (n: Person) RETURN CASE WHEN n.eyes = 'blue' THEN 1 WHEN n.age < 40 THEN 2 ELSE 3 END" + " AS result, n.eyes, n.age", + 'CASE (null values)': "MATCH (n: Person) RETURN n.name, CASE WHEN n.age IS NULL THEN -1 ELSE n.age - 10 END" + " AS age_10_years_ago", + 'CASE (with set)': "MATCH (n: Person) WITH n, CASE n.eyes WHEN 'blue' THEN 1 WHEN 'brown' THEN 2 ELSE 3 END " + "AS colorCode SET n.colorCode = colorCode RETURN n.name, n.colorCode" } diff --git a/tests/unit/test_generated.py b/tests/unit/test_generated.py new file mode 100644 index 0000000..3266804 --- /dev/null +++ b/tests/unit/test_generated.py @@ -0,0 +1,33 @@ +import pytest +from cymple import QueryBuilder + +qb = QueryBuilder() + +rendered = { + 'CASE_1': qb.reset().match().node(ref_name='p', labels='Person').return_mapping([('p.name', 'name'), ('CASE p.age WHEN 18 THEN "Young Adult" WHEN 65 THEN "Senior" ELSE "Adult" END', 'ageCategory')]), + 'CASE_2': qb.reset().match().node(ref_name='p', labels='Person').return_mapping([('p.name', 'name'), ('CASE WHEN p.age < 18 THEN "Minor" WHEN p.age >= 18 AND p.age < 65 THEN "Adult" ELSE "Senior" END', 'ageGroup')]), + 'CASE_3': qb.reset().match().node(ref_name='p', labels='Person').set({'p.category': 'CASE WHEN p.age < 18 THEN "Minor" WHEN p.age >= 18 AND p.age < 65 THEN "Adult" ELSE "Senior" END'}, escape_values=False).return_literal('p'), + 'CASE_4': qb.reset().match().node(ref_name='p', labels='Person').with_('p,').case(when_then_mapping={"\"America\"": "\"US\"", "\"North America\"": "\"Canada\""}, default_result="\"Other\"", results_ref='region', test_expression='p.country').return_literal('p.name, region'), + 'CASE_5': qb.reset().match().node(ref_name='p', labels='Person').return_mapping([('p.name', 'name'), ('CASE WHEN p.age < 18 THEN CASE p.age WHEN 0 THEN "Newborn" ELSE "Child" END WHEN p.age < 65 THEN "Adult" ELSE "Senior" END', 'lifeStage')]), + 'CASE_6': qb.reset().match().node(ref_name='p', labels='Person').return_mapping([('COUNT(CASE WHEN p.age < 18 THEN 1 END)', 'minors'), ('COUNT(CASE WHEN p.age >= 18 AND p.age < 65 THEN 1 END)', 'adults'), ('COUNT(CASE WHEN p.age >= 65 THEN 1 END)', 'seniors')]), + 'CASE_7': qb.reset().match().node(ref_name='p', labels='Person').where_literal('CASE WHEN p.age < 18 THEN false ELSE true END').return_literal('p'), + 'CASE_8': qb.reset().match().node(ref_name='p', labels='Person').return_mapping([('p.name', 'name'), ('p.age', 'age')]).order_by('CASE WHEN p.age < 18 THEN 1 WHEN p.age >= 18 AND p.age < 65 THEN 2 ELSE 3 END'), + 'CASE_9': qb.reset().match().node(ref_name='p', labels='Person').return_mapping([('p.name', 'name'), ('p.age', 'age'), ('CASE WHEN p.age < 18 THEN "Young " + p.name WHEN p.age >= 65 THEN "Senior " + p.name ELSE "Adult " + p.name END', 'titledName')]) +} + +expected = { + 'CASE_1': 'MATCH (p: Person) RETURN p.name AS name, CASE p.age WHEN 18 THEN "Young Adult" WHEN 65 THEN "Senior" ELSE "Adult" END AS ageCategory', + 'CASE_2': 'MATCH (p: Person) RETURN p.name AS name, CASE WHEN p.age < 18 THEN "Minor" WHEN p.age >= 18 AND p.age < 65 THEN "Adult" ELSE "Senior" END AS ageGroup', + 'CASE_3': 'MATCH (p: Person) SET p.category = CASE WHEN p.age < 18 THEN "Minor" WHEN p.age >= 18 AND p.age < 65 THEN "Adult" ELSE "Senior" END RETURN p', + 'CASE_4': 'MATCH (p: Person) WITH p, CASE p.country WHEN "US" THEN "America" WHEN "Canada" THEN "North America" ELSE "Other" END AS region RETURN p.name, region', + 'CASE_5': 'MATCH (p: Person) RETURN p.name AS name, CASE WHEN p.age < 18 THEN CASE p.age WHEN 0 THEN "Newborn" ELSE "Child" END WHEN p.age < 65 THEN "Adult" ELSE "Senior" END AS lifeStage', + 'CASE_6': 'MATCH (p: Person) RETURN COUNT(CASE WHEN p.age < 18 THEN 1 END) AS minors, COUNT(CASE WHEN p.age >= 18 AND p.age < 65 THEN 1 END) AS adults, COUNT(CASE WHEN p.age >= 65 THEN 1 END) AS seniors', + 'CASE_7': 'MATCH (p: Person) WHERE CASE WHEN p.age < 18 THEN false ELSE true END RETURN p', + 'CASE_8': 'MATCH (p: Person) RETURN p.name AS name, p.age AS age ORDER BY CASE WHEN p.age < 18 THEN 1 WHEN p.age >= 18 AND p.age < 65 THEN 2 ELSE 3 END ASC', + 'CASE_9': 'MATCH (p: Person) RETURN p.name AS name, p.age AS age, CASE WHEN p.age < 18 THEN "Young " + p.name WHEN p.age >= 65 THEN "Senior " + p.name ELSE "Adult " + p.name END AS titledName' +} + + +@pytest.mark.parametrize('clause', expected) +def test_samples(clause: str): + assert str(rendered[clause]) == expected[clause] diff --git a/tests/unit/test_real_use_cases.py b/tests/unit/test_real_use_cases.py index 211cb33..3b51db4 100644 --- a/tests/unit/test_real_use_cases.py +++ b/tests/unit/test_real_use_cases.py @@ -215,7 +215,7 @@ def cypher_relationship_properties(service_id): def test_cypher_get_all_findings(): - expected_query = f'MATCH (n: {Labels.Finding}) RETURN n as n' + expected_query = f'MATCH (n: {Labels.Finding}) RETURN n AS n' actual_query = cypher_get_all_findings() assert actual_query == expected_query @@ -224,22 +224,22 @@ def test_cypher_get_findings(): expected_query = ( f'MATCH (f: {Labels.Finding})-[: {Relations.has_finding_type}]->' f'(t: {Labels.FindingType})-[: {Relations.has_cve}]->(ct: {Labels.CVEType}) ' - f'RETURN f.{Properties.has_id} as id, t.{Properties.has_probability} as probability, ' - f't.{Properties.has_severity} as severity, ct.{Properties.has_id} as cve') + f'RETURN f.{Properties.has_id} AS id, t.{Properties.has_probability} AS probability, ' + f't.{Properties.has_severity} AS severity, ct.{Properties.has_id} AS cve') actual_query = cypher_get_findings() assert actual_query == expected_query def test_cypher_get_services(): expected_query = (f'MATCH (s: {Labels.ServiceType}) ' - f'RETURN s.{Properties.has_id} as id, s.{Properties.has_name} as name') + f'RETURN s.{Properties.has_id} AS id, s.{Properties.has_name} AS name') actual_query = cypher_get_services() assert actual_query == expected_query def test_cypher_get_business_applications(): expected_query = (f'MATCH (a: {Labels.Application}) ' - f'RETURN a.{Properties.has_id} as id, a.{Properties.has_name} as name') + f'RETURN a.{Properties.has_id} AS id, a.{Properties.has_name} AS name') actual_query = cypher_get_business_applications() assert actual_query == expected_query @@ -251,11 +251,11 @@ def test_cypher_get_finding_detailed(): f'MATCH (f: {Labels.Finding} {{{Properties.has_id} : "{finding_id}"}})-' f'[: {Relations.has_finding_type}]->(t: {Labels.FindingType})' f'-[: {Relations.has_cve}]->(ct: {Labels.CVEType}) ' - f'RETURN f.{Properties.has_id} as id, ' - f't.{Properties.has_probability} as probability, ' - f't.{Properties.has_severity} as severity, ct.{Properties.has_id} as cve, ' - f't.{Properties.has_description} as description, ' - f't.{Properties.has_attack_scenario} as attack_scenario, t.recommendation as recommendations') + f'RETURN f.{Properties.has_id} AS id, ' + f't.{Properties.has_probability} AS probability, ' + f't.{Properties.has_severity} AS severity, ct.{Properties.has_id} AS cve, ' + f't.{Properties.has_description} AS description, ' + f't.{Properties.has_attack_scenario} AS attack_scenario, t.recommendation AS recommendations') actual_query = cypher_get_finding_detailed(finding_id) assert actual_query == expected_query @@ -264,7 +264,7 @@ def test_cypher_get_finding_detailed(): def test_cypher_get_business_application(): application_id = 'THAT_is_MOCK_id' expected_query = (f'MATCH (a: {Labels.Application} {{{Properties.has_id} : "{application_id}"}}) ' - f'RETURN a.{Properties.has_id} as id, a.{Properties.has_name} as name') + f'RETURN a.{Properties.has_id} AS id, a.{Properties.has_name} AS name') actual_query = cypher_get_business_application(application_id) assert actual_query == expected_query @@ -274,7 +274,7 @@ def test_cypher_get_references_for_finding(): expected_query = (f'MATCH (: {Labels.Finding} {{{Properties.has_id} : "{finding_id}"}})' f'-[: {Relations.has_reference}]->(r: {Labels.ReferenceType}) ' - f'RETURN r.{Properties.has_description} as reference') + f'RETURN r.{Properties.has_description} AS reference') actual_query = cypher_get_references_for_finding(finding_id) assert actual_query == expected_query @@ -286,7 +286,7 @@ def test_cypher_get_applications_for_finding(): expected_query = ( f'MATCH (: {Labels.Finding} {{{Properties.has_id} : "{finding_id}"}})' f'-[: {Relations.has_cloud_object}]->(a: {Labels.Application}) ' - f'RETURN a.{Properties.has_name} as name') + f'RETURN a.{Properties.has_name} AS name') actual_query = cypher_get_applications_for_finding(finding_id) assert actual_query == expected_query @@ -299,8 +299,8 @@ def test_cypher_get_finding_type_details_for_service(): f'MATCH (t: {Labels.FindingType})<-[: {Relations.has_finding_type}]-' f'(: {Labels.Finding})-[: {Relations.has_cloud_object}]->' f'(: {Labels.CloudObject})-[: {Relations.has_service}]->( {{{Properties.has_id} : "{service_id}"}}) ' - f'RETURN collect(t.{Properties.has_severity}) as severities, ' - f'collect(t.{Properties.has_probability}) as probabilities') + f'RETURN collect(t.{Properties.has_severity}) AS severities, ' + f'collect(t.{Properties.has_probability}) AS probabilities') actual_query = cypher_get_finding_type_details_per_id(service_id) assert actual_query == expected_query @@ -391,6 +391,6 @@ def test_multiple_where_2(): assert actual_query == expected_query def test_with_list(): - expected_query = 'MATCH (n) WITH n as n_test WITH [n_test] as n_list RETURN n_list as n_test' - actual_query = str(QueryBuilder().match().node(ref_name='n').with_('n as n_test').with_('[n_test] as n_list').return_mapping(('n_list', 'n_test'))) + expected_query = 'MATCH (n) WITH n AS n_test WITH [n_test] AS n_list RETURN n_list AS n_test' + actual_query = str(QueryBuilder().match().node(ref_name='n').with_('n AS n_test').with_('[n_test] AS n_list').return_mapping(('n_list', 'n_test'))) assert actual_query == expected_query \ No newline at end of file