Skip to content

Commit ce126f7

Browse files
test(spp_cel_domain): cover SQL CASE/cache/smart-op edge paths (codecov patch target)
1 parent dd1bf96 commit ce126f7

1 file changed

Lines changed: 211 additions & 0 deletions

File tree

spp_cel_domain/tests/test_cel_sql_case.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from odoo.tests import TransactionCase, tagged
55
from odoo.tools.sql import SQL
66

7+
from ..services import cel_parser as P
8+
79

810
@tagged("post_install", "-at_install")
911
class TestToSqlCase(TransactionCase):
@@ -135,6 +137,215 @@ def test_boolean_connectives(self):
135137
sql_str = self._sql_to_str(result)
136138
self.assertIn("AND", sql_str)
137139

140+
def test_or_connective(self):
141+
"""Test || in conditions compiles to SQL OR."""
142+
expr = 'r.is_group == true || r.active == true ? "either" : "neither"'
143+
result = self.translator.to_sql_case(expr, "res.partner", "p")
144+
self.assertIsNotNone(result)
145+
sql_str = self._sql_to_str(result)
146+
self.assertIn("OR", sql_str)
147+
148+
def test_not_expression(self):
149+
"""Test ! compiles to SQL NOT."""
150+
# '!' is greedy in CEL, so it wraps the whole ternary; the compiled
151+
# SQL negates the CASE expression.
152+
expr = '! r.active == true ? "inactive" : "active"'
153+
result = self.translator.to_sql_case(expr, "res.partner", "p")
154+
self.assertIsNotNone(result)
155+
sql_str = self._sql_to_str(result)
156+
self.assertIn("NOT", sql_str)
157+
self.assertIn("CASE WHEN", sql_str)
158+
159+
def test_not_unsupported_operand_returns_none(self):
160+
"""Test NOT over an unsupported expression returns None."""
161+
expr = "!(r.color + 1 > 2)"
162+
result = self.translator.to_sql_case(expr, "res.partner", "p")
163+
self.assertIsNone(result)
164+
165+
def test_and_unsupported_operand_returns_none(self):
166+
"""Test && with an uncompilable operand falls back to None."""
167+
expr = 'r.color + 1 > 2 && r.active == true ? "a" : "b"'
168+
result = self.translator.to_sql_case(expr, "res.partner", "p")
169+
self.assertIsNone(result)
170+
171+
def test_or_unsupported_operand_returns_none(self):
172+
"""Test || with an uncompilable operand falls back to None."""
173+
expr = 'r.color + 1 > 2 || r.active == true ? "a" : "b"'
174+
result = self.translator.to_sql_case(expr, "res.partner", "p")
175+
self.assertIsNone(result)
176+
177+
def test_null_value_in_result(self):
178+
"""Test null as a ternary result compiles to SQL NULL."""
179+
expr = 'r.active == true ? null : "inactive"'
180+
result = self.translator.to_sql_case(expr, "res.partner", "p")
181+
self.assertIsNotNone(result)
182+
sql_str = self._sql_to_str(result)
183+
self.assertIn("NULL", sql_str)
184+
185+
def test_numeric_literal_comparison(self):
186+
"""Test comparison against a numeric literal compiles to SQL."""
187+
expr = 'r.color > 5 ? "big" : "small"'
188+
result = self.translator.to_sql_case(expr, "res.partner", "p")
189+
self.assertIsNotNone(result)
190+
sql_str = self._sql_to_str(result)
191+
self.assertIn("color", sql_str)
192+
193+
def test_bare_field_ident(self):
194+
"""Test a bare field name (no r. prefix) resolves to a column."""
195+
expr = 'active == true ? "on" : "off"'
196+
result = self.translator.to_sql_case(expr, "res.partner", "p")
197+
self.assertIsNotNone(result)
198+
sql_str = self._sql_to_str(result)
199+
self.assertIn("active", sql_str)
200+
201+
def test_bare_r_returns_none(self):
202+
"""Test 'r' alone is not a value expression and returns None."""
203+
expr = 'r == null ? "a" : "b"'
204+
result = self.translator.to_sql_case(expr, "res.partner", "p")
205+
self.assertIsNone(result)
206+
207+
def test_list_literal_returns_none(self):
208+
"""Test a list literal as a ternary result returns None."""
209+
expr = 'r.active == true ? [1] : "x"'
210+
result = self.translator.to_sql_case(expr, "res.partner", "p")
211+
self.assertIsNone(result)
212+
213+
def test_unsupported_then_returns_none(self):
214+
"""Test an uncompilable THEN branch falls back to None."""
215+
expr = 'r.active == true ? r.color + 1 : "x"'
216+
result = self.translator.to_sql_case(expr, "res.partner", "p")
217+
self.assertIsNone(result)
218+
219+
def test_unsupported_else_returns_none(self):
220+
"""Test an uncompilable ELSE branch falls back to None."""
221+
expr = 'r.active == true ? "x" : r.color + 1'
222+
result = self.translator.to_sql_case(expr, "res.partner", "p")
223+
self.assertIsNone(result)
224+
225+
def test_null_on_left_eq(self):
226+
"""Test null == field compiles to IS NULL."""
227+
expr = 'null == r.birthdate ? "unknown" : "known"'
228+
result = self.translator.to_sql_case(expr, "res.partner", "ind")
229+
self.assertIsNotNone(result)
230+
sql_str = self._sql_to_str(result)
231+
self.assertIn("IS NULL", sql_str)
232+
233+
def test_null_on_left_ne(self):
234+
"""Test null != field compiles to IS NOT NULL."""
235+
expr = 'null != r.birthdate ? "known" : "unknown"'
236+
result = self.translator.to_sql_case(expr, "res.partner", "ind")
237+
self.assertIsNotNone(result)
238+
sql_str = self._sql_to_str(result)
239+
self.assertIn("IS NOT NULL", sql_str)
240+
241+
def test_null_ordering_comparison_returns_none(self):
242+
"""Test ordering comparisons against null are not compilable."""
243+
for expr in (
244+
'r.birthdate > null ? "a" : "b"',
245+
'null < r.birthdate ? "a" : "b"',
246+
):
247+
result = self.translator.to_sql_case(expr, "res.partner", "p")
248+
self.assertIsNone(result, f"Expected None for {expr}")
249+
250+
def test_null_compared_to_unsupported_operand_returns_none(self):
251+
"""Test null compared to an uncompilable operand returns None."""
252+
expr = 'null == r.gender_id.code ? "a" : "b"'
253+
result = self.translator.to_sql_case(expr, "res.partner", "p")
254+
self.assertIsNone(result)
255+
256+
def test_nested_attr_returns_none(self):
257+
"""Test nested relational field access (r.a.b) is unsupported."""
258+
expr = 'r.gender_id.code == "F" ? "female" : "other"'
259+
result = self.translator.to_sql_case(expr, "res.partner", "p")
260+
self.assertIsNone(result)
261+
262+
def test_age_years_less_equal(self):
263+
"""Test age_years(r.birthdate) <= N inverts to >= cutoff."""
264+
expr = 'age_years(r.birthdate) <= 17 ? "minor" : "adult"'
265+
result = self.translator.to_sql_case(expr, "res.partner", "p")
266+
self.assertIsNotNone(result)
267+
sql_str = self._sql_to_str(result)
268+
self.assertIn("17 years", sql_str)
269+
# age <= 17 => birthdate >= cutoff
270+
self.assertIn(">=", sql_str)
271+
272+
def test_age_years_greater_than(self):
273+
"""Test age_years(r.birthdate) > N inverts to < cutoff."""
274+
expr = 'age_years(r.birthdate) > 59 ? "senior" : "other"'
275+
result = self.translator.to_sql_case(expr, "res.partner", "p")
276+
self.assertIsNotNone(result)
277+
sql_str = self._sql_to_str(result)
278+
self.assertIn("59 years", sql_str)
279+
# age > 59 => birthdate < cutoff
280+
self.assertIn("<", sql_str)
281+
282+
def test_age_years_ne(self):
283+
"""Test age_years(r.birthdate) != N compiles to an OR of two bounds."""
284+
expr = 'age_years(r.birthdate) != 30 ? "not_30" : "is_30"'
285+
result = self.translator.to_sql_case(expr, "res.partner", "p")
286+
self.assertIsNotNone(result)
287+
sql_str = self._sql_to_str(result)
288+
self.assertIn("30 years", sql_str)
289+
self.assertIn("31 years", sql_str)
290+
self.assertIn("OR", sql_str)
291+
292+
def test_age_years_non_literal_bound_returns_none(self):
293+
"""Test age_years() compared to a non-literal returns None."""
294+
expr = 'age_years(r.birthdate) < r.color ? "a" : "b"'
295+
result = self.translator.to_sql_case(expr, "res.partner", "p")
296+
self.assertIsNone(result)
297+
298+
def test_age_years_uncompilable_field_returns_none(self):
299+
"""Test age_years() over a nested field returns None."""
300+
expr = 'age_years(r.parent_id.birthdate) < 18 ? "a" : "b"'
301+
result = self.translator.to_sql_case(expr, "res.partner", "p")
302+
self.assertIsNone(result)
303+
304+
305+
@tagged("post_install", "-at_install")
306+
class TestAstToSqlExprDefensiveBranches(TransactionCase):
307+
"""Cover defensive AST branches unreachable through the CEL parser.
308+
309+
The parser lexes true/false to boolean Literals and never emits
310+
Literal(None) or Compare ops outside the six relational kinds, but
311+
_ast_to_sql_expr guards those shapes for callers constructing ASTs
312+
directly.
313+
"""
314+
315+
def setUp(self):
316+
super().setUp()
317+
self.translator = self.env["spp.cel.translator"]
318+
319+
def _expr(self, node):
320+
return self.translator._ast_to_sql_expr(node, "res.partner", "p", {}, {})
321+
322+
def test_literal_none(self):
323+
"""Test Literal(None) compiles to SQL NULL."""
324+
result = self._expr(P.Literal(None))
325+
self.assertIsNotNone(result)
326+
self.assertIn("NULL", str(result))
327+
328+
def test_ident_true_false(self):
329+
"""Test Ident('true')/Ident('false') compile to TRUE/FALSE."""
330+
self.assertIn("TRUE", str(self._expr(P.Ident("true"))))
331+
self.assertIn("FALSE", str(self._expr(P.Ident("false"))))
332+
333+
def test_not_none_operand(self):
334+
"""Test Not over an uncompilable node returns None."""
335+
node = P.Not(P.BinOp("ADD", P.Attr(P.Ident("r"), "color"), P.Literal(1)))
336+
self.assertIsNone(self._expr(node))
337+
338+
def test_compare_unsupported_op_returns_none(self):
339+
"""Test a Compare node with an unknown op returns None."""
340+
node = P.Compare("IN", P.Attr(P.Ident("r"), "color"), P.Literal(1))
341+
self.assertIsNone(self._expr(node))
342+
343+
def test_age_years_unsupported_op_returns_none(self):
344+
"""Test age_years comparison with an unknown op returns None."""
345+
call = P.Call(P.Ident("age_years"), [P.Attr(P.Ident("r"), "birthdate")])
346+
node = P.Compare("XX", call, P.Literal(5))
347+
self.assertIsNone(self._expr(node))
348+
138349

139350
@tagged("post_install", "-at_install")
140351
class TestSQLBuilderCaseWhen(TransactionCase):

0 commit comments

Comments
 (0)