Merge lp:~cjwatson/storm/is-and-is-not-operators into lp:storm

Proposed by Colin Watson
Status: Merged
Merged at revision: 577
Proposed branch: lp:~cjwatson/storm/is-and-is-not-operators
Merge into: lp:storm
Diff against target: 233 lines (+173/-0)
3 files modified
NEWS (+1/-0)
storm/expr.py (+70/-0)
storm/tests/expr.py (+102/-0)
To merge this branch: bzr merge lp:~cjwatson/storm/is-and-is-not-operators
Reviewer Review Type Date Requested Status
Andrey Fedoseev (community) Approve
Review via email: mp+431754@code.launchpad.net

Commit message

Add storm.expr.Is and storm.expr.IsNot operators.

Description of the change

Launchpad has had custom `IsTrue` and `IsFalse` operators for a while, because there are a few places where `expr` or `expr == True` (or the equivalents for False) don't quite do what we need: for example, if you're working with a nullable boolean column, then SQL NULL semantics mean that `expr = TRUE` or `expr = FALSE` return NULL when `expr` is NULL, which can be undesirable at times.

In addition, `==` and `!=` comparisons with True/False/None are typically picked up as errors by modern linters, since in normal Python code they should use `is` or `is not` or simple boolean tests instead. Having to override this is an annoyance.

Compare https://docs.sqlalchemy.org/en/20/core/operators.html#identity-comparisons, although SQLAlchemy is more permissive; it seems to be happy to compile anything on the right-hand-side of `IS` or `IS NOT` and leave it up to the database. Since we already have to take some care with how booleans are rendered, and there's no guarantee that the spelling of `IS TRUE` etc. is going to match the way that boolean values themselves are spelled, I thought it better to limit the set of right-hand-side expressions that we will compile.

I've checked that current versions of all the databases currently supported by Storm (PostgreSQL, MySQL, and SQLite) support all the resulting expressions, although in the case of SQLite it does need at least version 3.23.0 (released in 2018).

To post a comment you must log in.
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2022-03-16 16:59:21 +0000
+++ NEWS 2022-10-18 17:38:52 +0000
@@ -6,6 +6,7 @@
66
7- Clarify exception when creating a property with both allow_none=False and7- Clarify exception when creating a property with both allow_none=False and
8 default=None.8 default=None.
9- Add storm.expr.Is and storm.expr.IsNot operators.
910
10Bug fixes11Bug fixes
11---------12---------
1213
=== modified file 'storm/expr.py'
--- storm/expr.py 2020-12-07 22:38:36 +0000
+++ storm/expr.py 2022-10-18 17:38:52 +0000
@@ -1047,6 +1047,74 @@
1047 return compile(expr.exprs, state, join=expr.oper.lower())1047 return compile(expr.exprs, state, join=expr.oper.lower())
10481048
10491049
1050class Is(BinaryOper):
1051 """The SQL C{IS ...} operators, e.g. C{IS NULL}.
1052
1053 C{Is(expr, None)} is synonymous with C{expr == None}, but is less likely
1054 to trip up linters.
1055
1056 Unlike C{expr} or C{expr == True}, C{Is(expr, True)} returns C{FALSE}
1057 when C{expr} is C{NULL}.
1058
1059 Unlike C{Not(expr)} or C{expr == False}, C{Is(expr, False)} returns
1060 C{FALSE} when C{expr} is C{NULL}.
1061 """
1062 __slots__ = ()
1063 oper = " IS "
1064
1065@compile.when(Is)
1066def compile_is(compile, is_, state):
1067 tokens = [compile(is_.expr1, state), "IS"]
1068 if is_.expr2 is None:
1069 tokens.append("NULL")
1070 elif is_.expr2 is True:
1071 tokens.append("TRUE")
1072 elif is_.expr2 is False:
1073 tokens.append("FALSE")
1074 else:
1075 raise CompileError("expr2 must be None, True, or False")
1076 return " ".join(tokens)
1077
1078@compile_python.when(Is)
1079def compile_is(compile, is_, state):
1080 return "%s is %s" % (compile(is_.expr1, state), compile(is_.expr2, state))
1081
1082
1083class IsNot(BinaryOper):
1084 """The SQL C{IS NOT ...} operators, e.g. C{IS NOT NULL}.
1085
1086 C{IsNot(expr, None)} is synonymous with C{expr != None}, but is less
1087 likely to trip up linters.
1088
1089 Unlike C{Not(expr)} or C{expr != True}, C{IsNot(expr, True)} returns
1090 C{TRUE} when C{expr} is C{NULL}.
1091
1092 Unlike C{expr} or C{expr != False}, C{IsNot(expr, False)} returns
1093 C{TRUE} when C{expr} is C{NULL}.
1094 """
1095 __slots__ = ()
1096 oper = " IS NOT "
1097
1098@compile.when(IsNot)
1099def compile_is_not(compile, is_not, state):
1100 tokens = [compile(is_not.expr1, state), "IS NOT"]
1101 if is_not.expr2 is None:
1102 tokens.append("NULL")
1103 elif is_not.expr2 is True:
1104 tokens.append("TRUE")
1105 elif is_not.expr2 is False:
1106 tokens.append("FALSE")
1107 else:
1108 raise CompileError("expr2 must be None, True, or False")
1109 return " ".join(tokens)
1110
1111@compile_python.when(IsNot)
1112def compile_is_not(compile, is_not, state):
1113 return "%s is not %s" % (
1114 compile(is_not.expr1, state), compile(is_not.expr2, state)
1115 )
1116
1117
1050class Eq(BinaryOper):1118class Eq(BinaryOper):
1051 __slots__ = ()1119 __slots__ = ()
1052 oper = " = "1120 oper = " = "
@@ -1553,6 +1621,7 @@
1553compile.set_precedence(20, SQL)1621compile.set_precedence(20, SQL)
1554compile.set_precedence(30, Or)1622compile.set_precedence(30, Or)
1555compile.set_precedence(40, And)1623compile.set_precedence(40, And)
1624compile.set_precedence(45, Is, IsNot)
1556compile.set_precedence(50, Eq, Ne, Gt, Ge, Lt, Le, Like, In)1625compile.set_precedence(50, Eq, Ne, Gt, Ge, Lt, Le, Like, In)
1557compile.set_precedence(60, LShift, RShift)1626compile.set_precedence(60, LShift, RShift)
1558compile.set_precedence(70, Add, Sub)1627compile.set_precedence(70, Add, Sub)
@@ -1560,6 +1629,7 @@
15601629
1561compile_python.set_precedence(10, Or)1630compile_python.set_precedence(10, Or)
1562compile_python.set_precedence(20, And)1631compile_python.set_precedence(20, And)
1632compile_python.set_precedence(25, Is, IsNot)
1563compile_python.set_precedence(30, Eq, Ne, Gt, Ge, Lt, Le, Like, In)1633compile_python.set_precedence(30, Eq, Ne, Gt, Ge, Lt, Le, Like, In)
1564compile_python.set_precedence(40, LShift, RShift)1634compile_python.set_precedence(40, LShift, RShift)
1565compile_python.set_precedence(50, Add, Sub)1635compile_python.set_precedence(50, Add, Sub)
15661636
=== modified file 'storm/tests/expr.py'
--- storm/tests/expr.py 2020-12-07 22:38:36 +0000
+++ storm/tests/expr.py 2022-10-18 17:38:52 +0000
@@ -247,6 +247,16 @@
247 like_expr = expr.contains_string(u"abc!!_%", case_sensitive=False)247 like_expr = expr.contains_string(u"abc!!_%", case_sensitive=False)
248 self.assertIs(False, like_expr.case_sensitive)248 self.assertIs(False, like_expr.case_sensitive)
249249
250 def test_is(self):
251 expr = Is(elem1, elem2)
252 self.assertEqual(expr.expr1, elem1)
253 self.assertEqual(expr.expr2, elem2)
254
255 def test_is_not(self):
256 expr = IsNot(elem1, elem2)
257 self.assertEqual(expr.expr1, elem1)
258 self.assertEqual(expr.expr2, elem2)
259
250 def test_eq(self):260 def test_eq(self):
251 expr = Eq(elem1, elem2)261 expr = Eq(elem1, elem2)
252 self.assertEqual(expr.expr1, elem1)262 self.assertEqual(expr.expr1, elem1)
@@ -1101,6 +1111,56 @@
1101 self.assertEqual(statement, "?")1111 self.assertEqual(statement, "?")
1102 self.assertVariablesEqual(state.parameters, [Variable("value")])1112 self.assertVariablesEqual(state.parameters, [Variable("value")])
11031113
1114 def test_is_null(self):
1115 expr = Is(Func1(), None)
1116 state = State()
1117 statement = compile(expr, state)
1118 self.assertEqual(statement, "func1() IS NULL")
1119 self.assertEqual(state.parameters, [])
1120
1121 def test_is_true(self):
1122 expr = Is(Func1(), True)
1123 state = State()
1124 statement = compile(expr, state)
1125 self.assertEqual(statement, "func1() IS TRUE")
1126 self.assertEqual(state.parameters, [])
1127
1128 def test_is_false(self):
1129 expr = Is(Func1(), False)
1130 state = State()
1131 statement = compile(expr, state)
1132 self.assertEqual(statement, "func1() IS FALSE")
1133 self.assertEqual(state.parameters, [])
1134
1135 def test_is_invalid(self):
1136 expr = Is(Func1(), "x")
1137 self.assertRaises(CompileError, compile, expr)
1138
1139 def test_is_not_null(self):
1140 expr = IsNot(Func1(), None)
1141 state = State()
1142 statement = compile(expr, state)
1143 self.assertEqual(statement, "func1() IS NOT NULL")
1144 self.assertEqual(state.parameters, [])
1145
1146 def test_is_not_true(self):
1147 expr = IsNot(Func1(), True)
1148 state = State()
1149 statement = compile(expr, state)
1150 self.assertEqual(statement, "func1() IS NOT TRUE")
1151 self.assertEqual(state.parameters, [])
1152
1153 def test_is_not_false(self):
1154 expr = IsNot(Func1(), False)
1155 state = State()
1156 statement = compile(expr, state)
1157 self.assertEqual(statement, "func1() IS NOT FALSE")
1158 self.assertEqual(state.parameters, [])
1159
1160 def test_is_not_invalid(self):
1161 expr = IsNot(Func1(), "x")
1162 self.assertRaises(CompileError, compile, expr)
1163
1104 def test_eq(self):1164 def test_eq(self):
1105 expr = Eq(Func1(), Func2())1165 expr = Eq(Func1(), Func2())
1106 state = State()1166 state = State()
@@ -2273,6 +2333,48 @@
2273 self.assertEqual(py_expr, "_0")2333 self.assertEqual(py_expr, "_0")
2274 self.assertEqual(state.parameters, ["value"])2334 self.assertEqual(state.parameters, ["value"])
22752335
2336 def test_is_null(self):
2337 expr = Is(Variable(True), None)
2338 state = State()
2339 statement = compile_python(expr, state)
2340 self.assertEqual(statement, "_0 is None")
2341 self.assertEqual(state.parameters, [True])
2342
2343 def test_is_true(self):
2344 expr = Is(Variable(True), True)
2345 state = State()
2346 statement = compile_python(expr, state)
2347 self.assertEqual(statement, "_0 is _1")
2348 self.assertEqual(state.parameters, [True, True])
2349
2350 def test_is_false(self):
2351 expr = Is(Variable(True), False)
2352 state = State()
2353 statement = compile_python(expr, state)
2354 self.assertEqual(statement, "_0 is _1")
2355 self.assertEqual(state.parameters, [True, False])
2356
2357 def test_is_not_null(self):
2358 expr = IsNot(Variable(True), None)
2359 state = State()
2360 statement = compile_python(expr, state)
2361 self.assertEqual(statement, "_0 is not None")
2362 self.assertEqual(state.parameters, [True])
2363
2364 def test_is_not_true(self):
2365 expr = IsNot(Variable(True), True)
2366 state = State()
2367 statement = compile_python(expr, state)
2368 self.assertEqual(statement, "_0 is not _1")
2369 self.assertEqual(state.parameters, [True, True])
2370
2371 def test_is_not_false(self):
2372 expr = IsNot(Variable(True), False)
2373 state = State()
2374 statement = compile_python(expr, state)
2375 self.assertEqual(statement, "_0 is not _1")
2376 self.assertEqual(state.parameters, [True, False])
2377
2276 def test_eq(self):2378 def test_eq(self):
2277 expr = Eq(Variable(1), Variable(2))2379 expr = Eq(Variable(1), Variable(2))
2278 state = State()2380 state = State()

Subscribers

People subscribed via source and target branches

to status/vote changes: