Skip to content

Commit 0174a7c

Browse files
author
Abel Milash
committed
Fix PropertyValues format and add edge case tests
1 parent 0dcf0bd commit 0174a7c

5 files changed

Lines changed: 207 additions & 12 deletions

File tree

src/PowerPlatform/Dataverse/models/filters.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
# In operator (Dataverse function)
2525
expr = filter_in("statecode", [0, 1, 2])
2626
print(expr.to_odata())
27-
# Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=[0, 1, 2])
27+
# Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=["0","1","2"])
2828
2929
# Negation
3030
expr = ~eq("statecode", 1)
@@ -225,7 +225,11 @@ def __init__(self, column: str, values: Sequence[Any]) -> None:
225225
self.values = list(values)
226226

227227
def to_odata(self) -> str:
228-
formatted = ", ".join(_format_value(v) for v in self.values)
228+
# PropertyValues is Collection(Edm.String)
229+
formatted = ",".join(
230+
f'"{v.value if isinstance(v, enum.Enum) else v}"'
231+
for v in self.values
232+
)
229233
return (
230234
f"Microsoft.Dynamics.CRM.In"
231235
f"(PropertyName='{self.column}',PropertyValues=[{formatted}])"
@@ -392,7 +396,7 @@ def filter_in(column: str, values: Sequence[Any]) -> FilterExpression:
392396
Example::
393397
394398
filter_in("statecode", [0, 1, 2]).to_odata()
395-
# "Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=[0, 1, 2])"
399+
# "Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=["0","1","2"])"
396400
"""
397401
return _InFilter(column, values)
398402

src/PowerPlatform/Dataverse/models/query_builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def filter_in(self, column: str, values: Sequence[Any]) -> QueryBuilder:
228228
229229
query = QueryBuilder("account").filter_in("statecode", [0, 1, 2])
230230
# Produces: Microsoft.Dynamics.CRM.In(
231-
# PropertyName='statecode',PropertyValues=[0, 1, 2])
231+
# PropertyName='statecode',PropertyValues=["0","1","2"])
232232
"""
233233
self._filter_parts.append(filters.filter_in(column, values))
234234
return self

tests/unit/models/test_filters.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,31 @@ def test_eq_uuid(self):
171171
"accountid eq 12345678-1234-1234-1234-123456789abc",
172172
)
173173

174+
def test_eq_datetime(self):
175+
dt = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
176+
self.assertEqual(
177+
eq("createdon", dt).to_odata(),
178+
"createdon eq 2024-01-15T10:30:00Z",
179+
)
180+
181+
def test_eq_int_enum(self):
182+
from enum import IntEnum
183+
184+
class Priority(IntEnum):
185+
LOW = 1
186+
HIGH = 2
187+
188+
self.assertEqual(eq("priority", Priority.HIGH).to_odata(), "priority eq 2")
189+
190+
def test_ne_string(self):
191+
self.assertEqual(ne("name", "Contoso").to_odata(), "name ne 'Contoso'")
192+
193+
def test_gt_negative(self):
194+
self.assertEqual(gt("temperature", -10).to_odata(), "temperature gt -10")
195+
196+
def test_gt_float(self):
197+
self.assertEqual(gt("revenue", 99.5).to_odata(), "revenue gt 99.5")
198+
174199

175200
class TestFunctionFilters(unittest.TestCase):
176201
"""Tests for string function filter factory functions."""
@@ -187,6 +212,12 @@ def test_endswith(self):
187212
def test_function_column_lowercased(self):
188213
self.assertEqual(contains("Name", "Corp").to_odata(), "contains(name, 'Corp')")
189214

215+
def test_contains_single_quotes(self):
216+
self.assertEqual(
217+
contains("name", "O'Brien").to_odata(),
218+
"contains(name, 'O''Brien')",
219+
)
220+
190221

191222
class TestBetween(unittest.TestCase):
192223
"""Tests for the between factory function."""
@@ -201,6 +232,20 @@ def test_between_dates(self):
201232
result = between("createdon", date(2024, 1, 1), date(2024, 12, 31)).to_odata()
202233
self.assertEqual(result, "(createdon ge 2024-01-01 and createdon le 2024-12-31)")
203234

235+
def test_between_floats(self):
236+
self.assertEqual(
237+
between("revenue", 100.5, 999.9).to_odata(),
238+
"(revenue ge 100.5 and revenue le 999.9)",
239+
)
240+
241+
def test_between_datetimes(self):
242+
start = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
243+
end = datetime(2024, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
244+
self.assertEqual(
245+
between("createdon", start, end).to_odata(),
246+
"(createdon ge 2024-01-01T00:00:00Z and createdon le 2024-12-31T23:59:59Z)",
247+
)
248+
204249

205250
class TestNullChecks(unittest.TestCase):
206251
"""Tests for is_null and is_not_null."""
@@ -234,31 +279,58 @@ class TestInFilter(unittest.TestCase):
234279
def test_filter_in_ints(self):
235280
self.assertEqual(
236281
filter_in("statecode", [0, 1, 2]).to_odata(),
237-
"Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=[0, 1, 2])",
282+
'Microsoft.Dynamics.CRM.In(PropertyName=\'statecode\',PropertyValues=["0","1","2"])',
238283
)
239284

240285
def test_filter_in_strings(self):
241286
self.assertEqual(
242287
filter_in("name", ["Contoso", "Fabrikam"]).to_odata(),
243-
"Microsoft.Dynamics.CRM.In(PropertyName='name',PropertyValues=['Contoso', 'Fabrikam'])",
288+
'Microsoft.Dynamics.CRM.In(PropertyName=\'name\',PropertyValues=["Contoso","Fabrikam"])',
244289
)
245290

246291
def test_filter_in_single_value(self):
247292
self.assertEqual(
248293
filter_in("statecode", [0]).to_odata(),
249-
"Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=[0])",
294+
"Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=[\"0\"])",
250295
)
251296

252297
def test_filter_in_column_lowercased(self):
253298
self.assertEqual(
254299
filter_in("StateCode", [0, 1]).to_odata(),
255-
"Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=[0, 1])",
300+
'Microsoft.Dynamics.CRM.In(PropertyName=\'statecode\',PropertyValues=["0","1"])',
256301
)
257302

258303
def test_filter_in_empty_raises(self):
259304
with self.assertRaises(ValueError):
260305
filter_in("statecode", [])
261306

307+
def test_filter_in_int_enum(self):
308+
from enum import IntEnum
309+
310+
class Priority(IntEnum):
311+
LOW = 1
312+
MEDIUM = 2
313+
HIGH = 3
314+
315+
self.assertEqual(
316+
filter_in("priority", [Priority.LOW, Priority.HIGH]).to_odata(),
317+
'Microsoft.Dynamics.CRM.In(PropertyName=\'priority\',PropertyValues=["1","3"])',
318+
)
319+
320+
def test_filter_in_strings_with_quotes(self):
321+
self.assertEqual(
322+
filter_in("name", ["O'Brien", "McDonald's"]).to_odata(),
323+
"Microsoft.Dynamics.CRM.In(PropertyName='name',PropertyValues=[\"O'Brien\",\"McDonald's\"])",
324+
)
325+
326+
def test_filter_in_uuids(self):
327+
uid1 = uuid.UUID("12345678-1234-1234-1234-123456789abc")
328+
uid2 = uuid.UUID("87654321-4321-4321-4321-cba987654321")
329+
self.assertEqual(
330+
filter_in("accountid", [uid1, uid2]).to_odata(),
331+
'Microsoft.Dynamics.CRM.In(PropertyName=\'accountid\',PropertyValues=["12345678-1234-1234-1234-123456789abc","87654321-4321-4321-4321-cba987654321"])',
332+
)
333+
262334

263335
class TestLogicalOperators(unittest.TestCase):
264336
"""Tests for &, |, ~ operator overloads."""
@@ -311,6 +383,13 @@ def test_or_with_non_expression_returns_not_implemented(self):
311383
result = eq("a", 1).__or__("not an expression")
312384
self.assertIs(result, NotImplemented)
313385

386+
def test_and_with_filter_in(self):
387+
expr = filter_in("statecode", [0, 1]) & gt("revenue", 100000)
388+
self.assertEqual(
389+
expr.to_odata(),
390+
'(Microsoft.Dynamics.CRM.In(PropertyName=\'statecode\',PropertyValues=["0","1"]) and revenue gt 100000)',
391+
)
392+
314393

315394
class TestStrAndRepr(unittest.TestCase):
316395
"""Tests for __str__ and __repr__."""

tests/unit/models/test_query_builder.py

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ def test_filter_eq_float(self):
7878
qb = QueryBuilder("account").filter_eq("revenue", 1000000.5)
7979
self.assertEqual(qb.build()["filter"], "revenue eq 1000000.5")
8080

81+
def test_filter_eq_datetime(self):
82+
from datetime import datetime, timezone
83+
84+
dt = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
85+
qb = QueryBuilder("account").filter_eq("createdon", dt)
86+
self.assertEqual(qb.build()["filter"], "createdon eq 2024-01-15T10:30:00Z")
87+
8188

8289
class TestFilterIn(unittest.TestCase):
8390
"""Tests for the filter_in() method."""
@@ -86,28 +93,28 @@ def test_filter_in_integers(self):
8693
qb = QueryBuilder("account").filter_in("statecode", [0, 1, 2])
8794
self.assertEqual(
8895
qb.build()["filter"],
89-
"Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=[0, 1, 2])",
96+
'Microsoft.Dynamics.CRM.In(PropertyName=\'statecode\',PropertyValues=["0","1","2"])',
9097
)
9198

9299
def test_filter_in_strings(self):
93100
qb = QueryBuilder("account").filter_in("name", ["Contoso", "Fabrikam"])
94101
self.assertEqual(
95102
qb.build()["filter"],
96-
"Microsoft.Dynamics.CRM.In(PropertyName='name',PropertyValues=['Contoso', 'Fabrikam'])",
103+
'Microsoft.Dynamics.CRM.In(PropertyName=\'name\',PropertyValues=["Contoso","Fabrikam"])',
97104
)
98105

99106
def test_filter_in_single_value(self):
100107
qb = QueryBuilder("account").filter_in("statecode", [0])
101108
self.assertEqual(
102109
qb.build()["filter"],
103-
"Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=[0])",
110+
"Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=[\"0\"])",
104111
)
105112

106113
def test_filter_in_column_lowercased(self):
107114
qb = QueryBuilder("account").filter_in("StateCode", [0, 1])
108115
self.assertEqual(
109116
qb.build()["filter"],
110-
"Microsoft.Dynamics.CRM.In(PropertyName='statecode',PropertyValues=[0, 1])",
117+
'Microsoft.Dynamics.CRM.In(PropertyName=\'statecode\',PropertyValues=["0","1"])',
111118
)
112119

113120
def test_filter_in_empty_raises(self):
@@ -118,6 +125,26 @@ def test_filter_in_returns_self(self):
118125
qb = QueryBuilder("account")
119126
self.assertIs(qb.filter_in("statecode", [0, 1]), qb)
120127

128+
def test_filter_in_int_enum(self):
129+
from enum import IntEnum
130+
131+
class Priority(IntEnum):
132+
LOW = 1
133+
HIGH = 3
134+
135+
qb = QueryBuilder("account").filter_in("priority", [Priority.LOW, Priority.HIGH])
136+
self.assertEqual(
137+
qb.build()["filter"],
138+
'Microsoft.Dynamics.CRM.In(PropertyName=\'priority\',PropertyValues=["1","3"])',
139+
)
140+
141+
def test_filter_in_combined_with_other_filters(self):
142+
qb = QueryBuilder("account").filter_eq("statecode", 0).filter_in("priority", [1, 2, 3])
143+
self.assertEqual(
144+
qb.build()["filter"],
145+
'statecode eq 0 and Microsoft.Dynamics.CRM.In(PropertyName=\'priority\',PropertyValues=["1","2","3"])',
146+
)
147+
121148
def test_filter_ne(self):
122149
qb = QueryBuilder("account").filter_ne("statecode", 1)
123150
self.assertEqual(qb.build()["filter"], "statecode ne 1")
@@ -168,6 +195,10 @@ def test_filter_endswith(self):
168195
qb = QueryBuilder("account").filter_endswith("name", "Ltd")
169196
self.assertEqual(qb.build()["filter"], "endswith(name, 'Ltd')")
170197

198+
def test_filter_contains_single_quotes(self):
199+
qb = QueryBuilder("account").filter_contains("name", "O'Brien")
200+
self.assertEqual(qb.build()["filter"], "contains(name, 'O''Brien')")
201+
171202

172203
class TestNullFilters(unittest.TestCase):
173204
"""Tests for null/not-null filter methods."""
@@ -209,6 +240,17 @@ def test_filter_between_combined_with_other_filters(self):
209240
"statecode eq 0 and (revenue ge 100000 and revenue le 500000)",
210241
)
211242

243+
def test_filter_between_datetimes(self):
244+
from datetime import datetime, timezone
245+
246+
start = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
247+
end = datetime(2024, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
248+
qb = QueryBuilder("account").filter_between("createdon", start, end)
249+
self.assertEqual(
250+
qb.build()["filter"],
251+
"(createdon ge 2024-01-01T00:00:00Z and createdon le 2024-12-31T23:59:59Z)",
252+
)
253+
212254

213255
class TestFilterRaw(unittest.TestCase):
214256
"""Tests for the filter_raw() method."""
@@ -277,6 +319,16 @@ def test_where_with_not(self):
277319
qb = QueryBuilder("account").where(~eq("statecode", 1))
278320
self.assertEqual(qb.build()["filter"], "not (statecode eq 1)")
279321

322+
def test_where_with_filter_in(self):
323+
from PowerPlatform.Dataverse.models.filters import filter_in, gt
324+
325+
expr = filter_in("statecode", [0, 1]) & gt("revenue", 100000)
326+
qb = QueryBuilder("account").where(expr)
327+
self.assertEqual(
328+
qb.build()["filter"],
329+
'(Microsoft.Dynamics.CRM.In(PropertyName=\'statecode\',PropertyValues=["0","1"]) and revenue gt 100000)',
330+
)
331+
280332

281333
class TestOrderBy(unittest.TestCase):
282334
"""Tests for the order_by() method."""
@@ -519,6 +571,22 @@ def test_execute_with_where_expressions(self):
519571
"((statecode eq 0 or statecode eq 1) and revenue gt 100000)",
520572
)
521573

574+
def test_execute_with_filter_in(self):
575+
mock_query_ops = MagicMock()
576+
mock_client = mock_query_ops._client
577+
mock_client.records.get.return_value = iter([])
578+
579+
qb = QueryBuilder("account")
580+
qb._query_ops = mock_query_ops
581+
qb.filter_in("statecode", [0, 1, 2])
582+
list(qb.execute())
583+
584+
call_args = mock_client.records.get.call_args
585+
self.assertEqual(
586+
call_args.kwargs["filter"],
587+
'Microsoft.Dynamics.CRM.In(PropertyName=\'statecode\',PropertyValues=["0","1","2"])',
588+
)
589+
522590

523591
if __name__ == "__main__":
524592
unittest.main()

tests/unit/test_query_operations.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,50 @@ def test_builder_execute_with_where(self):
150150
"((statecode eq 0 or statecode eq 1) and revenue gt 100000)",
151151
)
152152

153+
def test_builder_execute_with_filter_in(self):
154+
"""builder().filter_in().execute() should forward CRM.In filter to _get_multiple."""
155+
self.client._odata._get_multiple.return_value = iter([[{"accountid": "1"}]])
156+
157+
list(self.client.query.builder("account").select("name").filter_in("statecode", [0, 1, 2]).execute())
158+
159+
call_kwargs = self.client._odata._get_multiple.call_args
160+
self.assertEqual(
161+
call_kwargs.kwargs["filter"],
162+
'Microsoft.Dynamics.CRM.In(PropertyName=\'statecode\',PropertyValues=["0","1","2"])',
163+
)
164+
165+
def test_builder_execute_with_where_filter_in(self):
166+
"""builder().where(filter_in(...) & ...).execute() should compile composed expression."""
167+
from PowerPlatform.Dataverse.models.filters import filter_in, gt
168+
169+
self.client._odata._get_multiple.return_value = iter([[{"accountid": "1"}]])
170+
171+
list(
172+
self.client.query.builder("account").where(filter_in("statecode", [0, 1]) & gt("revenue", 100000)).execute()
173+
)
174+
175+
call_kwargs = self.client._odata._get_multiple.call_args
176+
self.assertEqual(
177+
call_kwargs.kwargs["filter"],
178+
'(Microsoft.Dynamics.CRM.In(PropertyName=\'statecode\',PropertyValues=["0","1"]) and revenue gt 100000)',
179+
)
180+
181+
def test_builder_execute_with_filter_between_datetimes(self):
182+
"""builder().filter_between() with datetimes should forward correct OData."""
183+
from datetime import datetime, timezone
184+
185+
self.client._odata._get_multiple.return_value = iter([[{"accountid": "1"}]])
186+
187+
start = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
188+
end = datetime(2024, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
189+
list(self.client.query.builder("account").filter_between("createdon", start, end).execute())
190+
191+
call_kwargs = self.client._odata._get_multiple.call_args
192+
self.assertEqual(
193+
call_kwargs.kwargs["filter"],
194+
"(createdon ge 2024-01-01T00:00:00Z and createdon le 2024-12-31T23:59:59Z)",
195+
)
196+
153197
def test_builder_full_fluent_workflow(self):
154198
"""End-to-end test of the fluent query workflow."""
155199
expected_records = [

0 commit comments

Comments
 (0)