Skip to content

Commit 9ce37a3

Browse files
committed
RDBC-1059 Add RavenDocumentQuery.now/today/cmp_xchg server-side helpers
1 parent b9a7724 commit 9ce37a3

7 files changed

Lines changed: 289 additions & 70 deletions

File tree

ravendb/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@
195195
from ravendb.documents.queries.highlighting import HighlightingOptions, QueryHighlightings
196196
from ravendb.documents.queries.index_query import IndexQuery
197197
from ravendb.documents.queries.misc import SearchOperator
198+
from ravendb.documents.queries.raven_document_query import RavenDocumentQuery
198199
from ravendb.documents.queries.more_like_this import (
199200
MoreLikeThisOperations,
200201
MoreLikeThisBase,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
from typing import Optional
4+
5+
from ravendb.documents.session.misc import MethodCall, CmpXchg
6+
from ravendb.documents.session.tokens.query_tokens.definitions import WhereToken
7+
8+
9+
class RavenDocumentQuery:
10+
"""Server-side RQL functions for DocumentQuery: now(), today(), cmpxchg()."""
11+
12+
@staticmethod
13+
def now(offset: Optional[str] = None) -> "RavenDocumentQuery.Time":
14+
if offset is None:
15+
return RavenDocumentQuery.Time(WhereToken.MethodsType.NOW)
16+
return RavenDocumentQuery.Time(WhereToken.MethodsType.NOW, [offset])
17+
18+
@staticmethod
19+
def today() -> "RavenDocumentQuery.Time":
20+
return RavenDocumentQuery.Time(WhereToken.MethodsType.TODAY)
21+
22+
@staticmethod
23+
def cmp_xchg(key: str) -> CmpXchg:
24+
# Build directly — CmpXchg.value() is deprecated and emits a warning.
25+
cmp_xchg = CmpXchg()
26+
cmp_xchg.args = [key]
27+
return cmp_xchg
28+
29+
class Time(MethodCall):
30+
def __init__(self, method_type: WhereToken.MethodsType, args=None):
31+
super().__init__(args=args or [])
32+
self.method_type = method_type

ravendb/documents/session/misc.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,16 @@ def __init__(self, args: List[object] = None, access_path: str = None):
303303
class CmpXchg(MethodCall):
304304
@classmethod
305305
def value(cls, key: str) -> CmpXchg:
306+
# Kept for back-compat; prefer RavenDocumentQuery.cmp_xchg().
307+
import warnings
308+
309+
warnings.warn(
310+
"CmpXchg.value is deprecated; use RavenDocumentQuery.cmp_xchg() instead.",
311+
DeprecationWarning,
312+
stacklevel=2,
313+
)
306314
cmp_xchg = cls()
307315
cmp_xchg.args = [key]
308-
309316
return cmp_xchg
310317

311318

ravendb/documents/session/query.py

Lines changed: 49 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ def _where_equals(
463463
tokens = self.__get_current_where_tokens()
464464
self.__append_operator_if_needed(tokens)
465465

466-
if self.__if_value_is_method(WhereOperator.EQUALS, params, tokens):
466+
if self._if_value_is_method(WhereOperator.EQUALS, params, tokens):
467467
return
468468

469469
transform_to_equal_value = self.__transform_value(params)
@@ -476,17 +476,21 @@ def _where_equals(
476476
)
477477
tokens.append(where_token)
478478

479-
def __if_value_is_method(self, op: WhereOperator, where_params: WhereParams, tokens: List[QueryToken]) -> bool:
479+
def _if_value_is_method(self, op: WhereOperator, where_params: WhereParams, tokens: List[QueryToken]) -> bool:
480+
# MethodCall values (RavenDocumentQuery.now/today, CmpXchg) emit a
481+
# method-flavored WhereToken instead of binding as a parameter.
482+
# Returns True if a token was appended (caller should short-circuit).
480483
if isinstance(where_params.value, MethodCall):
484+
from ravendb.documents.queries.raven_document_query import RavenDocumentQuery
485+
481486
mc = where_params.value
482487

483488
args = []
484489
for arg in mc.args:
485490
args.append(self.__add_query_parameter(arg))
486491

487492
token: Optional[WhereToken] = None
488-
object_type = type(mc)
489-
if object_type == CmpXchg:
493+
if isinstance(mc, CmpXchg):
490494
token = WhereToken.create(
491495
op,
492496
where_params.field_name,
@@ -500,8 +504,22 @@ def __if_value_is_method(self, op: WhereOperator, where_params: WhereParams, tok
500504
)
501505
),
502506
)
507+
elif isinstance(mc, RavenDocumentQuery.Time):
508+
token = WhereToken.create(
509+
op,
510+
where_params.field_name,
511+
None,
512+
WhereToken.WhereOptions(
513+
method_type__parameters__property__exact=(
514+
mc.method_type,
515+
args,
516+
mc.access_path,
517+
where_params.exact,
518+
)
519+
),
520+
)
503521
else:
504-
raise TypeError(f"Unknown method {object_type}")
522+
raise TypeError(f"Unknown method {type(mc)}")
505523

506524
tokens.append(token)
507525
return True
@@ -537,7 +555,7 @@ def _where_not_equals(
537555

538556
where_params.field_name = self._ensure_valid_field_name(where_params.field_name, where_params.nested_path)
539557

540-
if self.__if_value_is_method(WhereOperator.NOT_EQUALS, where_params, tokens):
558+
if self._if_value_is_method(WhereOperator.NOT_EQUALS, where_params, tokens):
541559
return
542560

543561
where_token = WhereToken.create(
@@ -639,82 +657,46 @@ def _where_between(self, field_name: str, start: object, end: object, exact: Opt
639657
)
640658
tokens.append(where_token)
641659

642-
def _where_greater_than(self, field_name: str, value: object, exact: Optional[bool] = False) -> None:
660+
def _where_compare(
661+
self,
662+
op: WhereOperator,
663+
field_name: str,
664+
value: object,
665+
exact: Optional[bool],
666+
null_sentinel: str,
667+
) -> None:
668+
# Shared body for >/>=/</<=. Routes through _if_value_is_method first
669+
# so MethodCall values (now/today/cmpxchg) are emitted as RQL calls.
643670
field_name = self._ensure_valid_field_name(field_name, False)
644-
645671
tokens = self.__get_current_where_tokens()
646672
self.__append_operator_if_needed(tokens)
647673
self.__negate_if_needed(tokens, field_name)
648674
where_params = WhereParams()
649675
where_params.value = value
650676
where_params.field_name = field_name
677+
where_params.exact = exact
651678

652-
parameter = self.__add_query_parameter("*" if value is None else self.__transform_value(where_params, True))
653-
where_token = WhereToken.create(
654-
WhereOperator.GREATER_THAN,
655-
field_name,
656-
parameter,
657-
WhereToken.WhereOptions(exact__from__to=(exact, None, None)),
658-
)
659-
tokens.append(where_token)
660-
661-
def _where_greater_than_or_equal(self, field_name: str, value: object, exact: Optional[bool] = False) -> None:
662-
field_name = self._ensure_valid_field_name(field_name, False)
663-
664-
tokens = self.__get_current_where_tokens()
665-
self.__append_operator_if_needed(tokens)
666-
self.__negate_if_needed(tokens, field_name)
667-
where_params = WhereParams()
668-
where_params.value = value
669-
where_params.field_name = field_name
679+
if self._if_value_is_method(op, where_params, tokens):
680+
return
670681

671-
parameter = self.__add_query_parameter("*" if value is None else self.__transform_value(where_params, True))
672-
where_token = WhereToken.create(
673-
WhereOperator.GREATER_THAN_OR_EQUAL,
674-
field_name,
675-
parameter,
676-
WhereToken.WhereOptions(exact__from__to=(exact, None, None)),
682+
parameter = self.__add_query_parameter(
683+
null_sentinel if value is None else self.__transform_value(where_params, True)
677684
)
678-
tokens.append(where_token)
679-
680-
def _where_less_than(self, field_name: str, value: object, exact: Optional[bool] = False) -> None:
681-
field_name = self._ensure_valid_field_name(field_name, False)
682-
683-
tokens = self.__get_current_where_tokens()
684-
self.__append_operator_if_needed(tokens)
685-
self.__negate_if_needed(tokens, field_name)
686-
where_params = WhereParams()
687-
where_params.value = value
688-
where_params.field_name = field_name
689-
690-
parameter = self.__add_query_parameter("*" if value is None else self.__transform_value(where_params, True))
691-
where_token = WhereToken.create(
692-
WhereOperator.LESS_THAN,
693-
field_name,
694-
parameter,
695-
WhereToken.WhereOptions(exact__from__to=(exact, None, None)),
685+
tokens.append(
686+
WhereToken.create(op, field_name, parameter, WhereToken.WhereOptions(exact__from__to=(exact, None, None)))
696687
)
697-
tokens.append(where_token)
698688

699-
def _where_less_than_or_equal(self, field_name: str, value: object, exact: Optional[bool] = False) -> None:
700-
field_name = self._ensure_valid_field_name(field_name, False)
689+
def _where_greater_than(self, field_name: str, value: object, exact: Optional[bool] = False) -> None:
690+
self._where_compare(WhereOperator.GREATER_THAN, field_name, value, exact, "*")
701691

702-
tokens = self.__get_current_where_tokens()
703-
self.__append_operator_if_needed(tokens)
704-
self.__negate_if_needed(tokens, field_name)
692+
def _where_greater_than_or_equal(self, field_name: str, value: object, exact: Optional[bool] = False) -> None:
693+
self._where_compare(WhereOperator.GREATER_THAN_OR_EQUAL, field_name, value, exact, "*")
705694

706-
where_params = WhereParams()
707-
where_params.value = value
708-
where_params.field_name = field_name
695+
def _where_less_than(self, field_name: str, value: object, exact: Optional[bool] = False) -> None:
696+
self._where_compare(WhereOperator.LESS_THAN, field_name, value, exact, "NULL")
709697

710-
parameter = self.__add_query_parameter("NULL" if value is None else self.__transform_value(where_params, True))
711-
where_token = WhereToken.create(
712-
WhereOperator.LESS_THAN_OR_EQUAL,
713-
field_name,
714-
parameter,
715-
WhereToken.WhereOptions(exact__from__to=(exact, None, None)),
716-
)
717-
tokens.append(where_token)
698+
def _where_less_than_or_equal(self, field_name: str, value: object, exact: Optional[bool] = False) -> None:
699+
self._where_compare(WhereOperator.LESS_THAN_OR_EQUAL, field_name, value, exact, "NULL")
718700

719701
def _where_regex(self, field_name: str, pattern: str) -> None:
720702
field_name = self._ensure_valid_field_name(field_name, False)

ravendb/documents/session/tokens/query_tokens/definitions.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,8 @@ def write_to(self, writer: List[str]):
576576
class WhereToken(QueryToken):
577577
class MethodsType(enum.Enum):
578578
CMP_X_CHG = "CmpXChg"
579+
NOW = "Now"
580+
TODAY = "Today"
579581

580582
class WhereMethodCall:
581583
def __init__(
@@ -674,10 +676,21 @@ def add_alias(self, alias: str) -> WhereToken:
674676

675677
def __write_method(self, writer: List[str]) -> bool:
676678
if self.options.method is not None:
677-
if self.options.method.method_type == WhereToken.MethodsType.CMP_X_CHG:
679+
method_type = self.options.method.method_type
680+
if method_type == WhereToken.MethodsType.NOW:
681+
writer.append("now(")
682+
if self.options.method.parameters:
683+
writer.append("$")
684+
writer.append(self.options.method.parameters[0])
685+
writer.append(")")
686+
return True
687+
if method_type == WhereToken.MethodsType.TODAY:
688+
writer.append("today()")
689+
return True
690+
if method_type == WhereToken.MethodsType.CMP_X_CHG:
678691
writer.append("cmpxchg(")
679692
else:
680-
raise ValueError(f"Unsupported method: {self.options.method.method_type}")
693+
raise ValueError(f"Unsupported method: {method_type}")
681694

682695
first = True
683696
for parameter in self.options.method.parameters:
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
Unit tests for RavenDocumentQuery.Now / Today / CmpXchg plus the
3+
WhereToken NOW/TODAY MethodsType output.
4+
"""
5+
6+
import unittest
7+
import warnings
8+
9+
from ravendb.documents.queries.raven_document_query import RavenDocumentQuery
10+
from ravendb.documents.session.misc import CmpXchg, MethodCall
11+
from ravendb.documents.session.tokens.misc import WhereOperator
12+
from ravendb.documents.session.tokens.query_tokens.definitions import WhereToken
13+
14+
15+
class TestRavenDocumentQueryFactories(unittest.TestCase):
16+
def test_now_no_args(self):
17+
t = RavenDocumentQuery.now()
18+
self.assertEqual(WhereToken.MethodsType.NOW, t.method_type)
19+
self.assertEqual([], t.args)
20+
self.assertIsInstance(t, MethodCall)
21+
22+
def test_now_with_offset(self):
23+
t = RavenDocumentQuery.now("+1d")
24+
self.assertEqual(WhereToken.MethodsType.NOW, t.method_type)
25+
self.assertEqual(["+1d"], t.args)
26+
27+
def test_today(self):
28+
t = RavenDocumentQuery.today()
29+
self.assertEqual(WhereToken.MethodsType.TODAY, t.method_type)
30+
self.assertEqual([], t.args)
31+
32+
def test_cmp_xchg_does_not_warn(self):
33+
with warnings.catch_warnings(record=True) as captured:
34+
warnings.simplefilter("always")
35+
x = RavenDocumentQuery.cmp_xchg("foo")
36+
self.assertFalse(any(issubclass(w.category, DeprecationWarning) for w in captured))
37+
self.assertIsInstance(x, CmpXchg)
38+
self.assertEqual(["foo"], x.args)
39+
40+
def test_legacy_cmp_xchg_value_emits_deprecation(self):
41+
with warnings.catch_warnings(record=True) as captured:
42+
warnings.simplefilter("always")
43+
CmpXchg.value("foo")
44+
self.assertTrue(any(issubclass(w.category, DeprecationWarning) for w in captured))
45+
46+
47+
class TestWhereTokenNowTodayOutput(unittest.TestCase):
48+
@staticmethod
49+
def _render(method_type: WhereToken.MethodsType, parameters):
50+
token = WhereToken.create(
51+
WhereOperator.GREATER_THAN,
52+
"CreatedAt",
53+
None,
54+
WhereToken.WhereOptions(method_type__parameters__property__exact=(method_type, parameters, None, None)),
55+
)
56+
out = []
57+
token.write_to(out)
58+
return "".join(out)
59+
60+
def test_now_no_param(self):
61+
self.assertEqual("CreatedAt > now()", self._render(WhereToken.MethodsType.NOW, []))
62+
63+
def test_now_with_param(self):
64+
self.assertEqual("CreatedAt > now($p0)", self._render(WhereToken.MethodsType.NOW, ["p0"]))
65+
66+
def test_today(self):
67+
self.assertEqual("CreatedAt > today()", self._render(WhereToken.MethodsType.TODAY, []))
68+
69+
70+
if __name__ == "__main__":
71+
unittest.main()

0 commit comments

Comments
 (0)