Skip to content

Commit dc1332d

Browse files
averikitschandrewsg
authored andcommitted
NDB: Query constructor (#7239)
* draft of Query class with notes * remove bad comment * Add docstring * update docstring
1 parent 5116890 commit dc1332d

2 files changed

Lines changed: 139 additions & 5 deletions

File tree

ndb/src/google/cloud/ndb/query.py

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -872,10 +872,120 @@ def resolve(self, bindings, used):
872872

873873

874874
class Query:
875-
__slots__ = ()
875+
"""Query object.
876876
877-
def __init__(self, *args, **kwargs):
878-
raise NotImplementedError
877+
Args:
878+
kind (str): The kind of entities to be queried.
879+
ancestor (Key): Entities returned will be descendants of `ancestor`.
880+
filters (Union[Node, tuple]): Node representing a filter expression
881+
tree. Property filters applied by this query. The sequence
882+
is ``(property_name, operator, value)``.
883+
orders (Union[datastore_query.Order, list]): The field names used to
884+
order query results. Renamed `order` in google.cloud.datastore.
885+
app (str): The namespace to restrict results. If not passed, uses the
886+
client's value. Renamed `project` in google.cloud.datastore.
887+
namespace (str): The namespace to which to restrict results.
888+
If not passed, uses the client's value.
889+
default_options (QueryOptions): QueryOptions object.
890+
projection (Union[list, tuple]): The fields returned as part of the
891+
query results.
892+
group_by (Union[list, tuple]): The field names used to group query
893+
results. Renamed distinct_on in google.cloud.datastore.
894+
895+
Raises: TypeError if any of the arguments are invalid.
896+
"""
897+
898+
def __init__(self, kind=None, ancestor=None, filters=None, orders=None,
899+
app=None, namespace=None, default_options=None,
900+
projection=None, group_by=None):
901+
if ancestor is not None:
902+
if isinstance(ancestor, ParameterizedThing):
903+
if isinstance(ancestor, ParameterizedFunction):
904+
if ancestor.func != 'key':
905+
raise TypeError("ancestor cannot be a GQL function"
906+
"other than Key")
907+
else:
908+
if not isinstance(ancestor, model.Key):
909+
raise TypeError("ancestor must be a Key; "
910+
F"received {ancestor}")
911+
if not ancestor.id():
912+
raise ValueError("ancestor cannot be an incomplete key")
913+
if app is not None:
914+
if app != ancestor.app():
915+
raise TypeError("ancestor/app id mismatch")
916+
else:
917+
app = ancestor.app()
918+
if namespace is not None:
919+
if namespace != ancestor.namespace():
920+
raise TypeError("ancestor/namespace mismatch")
921+
else:
922+
namespace = ancestor.namespace()
923+
if filters is not None:
924+
if not isinstance(filters, Node):
925+
raise TypeError("filters must be a query Node or None; "
926+
F"received {filters}")
927+
if orders is not None:
928+
if not isinstance(orders, (list,)): # datastore_query.Order
929+
raise TypeError("orders must be an Order instance or None; "
930+
F"received {orders}")
931+
# if default_options is not None: # Optional QueryOptions object.
932+
# if not isinstance(default_options, datastore_rpc.BaseConfiguration):
933+
# raise TypeError("default_options must be a Configuration or None; "
934+
# F"received {default_options}")
935+
# if projection is not None:
936+
# if default_options.projection is not None:
937+
# raise TypeError("cannot use projection keyword argument and "
938+
# "default_options.projection at the same time")
939+
# if default_options.keys_only is not None:
940+
# raise TypeError("cannot use projection keyword argument and "
941+
# "default_options.keys_only at the same time")
942+
943+
self.kind = kind
944+
self.ancestor = ancestor
945+
self.filters = filters
946+
self.orders = orders
947+
self.app = app
948+
self.namespace = namespace
949+
self.default_options = default_options
950+
951+
self.projection = None
952+
if projection is not None:
953+
if not projection:
954+
raise TypeError('projection argument cannot be empty')
955+
if not isinstance(projection, (tuple, list)):
956+
raise TypeError("projection must be a tuple, list or None; "
957+
F"received {projection}")
958+
self._check_properties(self._to_property_names(projection))
959+
self.projection = tuple(projection)
960+
961+
self.group_by = None
962+
if group_by is not None:
963+
if not group_by:
964+
raise TypeError('group_by argument cannot be empty')
965+
if not isinstance(group_by, (tuple, list)):
966+
raise TypeError("group_by must be a tuple, list or None; "
967+
F"received {group_by}")
968+
self._check_properties(self._to_property_names(group_by))
969+
self.group_by = tuple(group_by)
970+
971+
def _to_property_names(self, properties):
972+
if not isinstance(properties, (list, tuple)):
973+
properties = [properties]
974+
fixed = []
975+
for prop in properties:
976+
if isinstance(prop, str):
977+
fixed.append(prop)
978+
elif isinstance(prop, model.Property):
979+
fixed.append(prop._name)
980+
else:
981+
raise TypeError(
982+
F"Unexpected property {prop}; should be string or Property")
983+
return fixed
984+
985+
def _check_properties(self, fixed, **kwargs):
986+
modelclass = model.Model._kind_map.get(self.__kind)
987+
if modelclass is not None:
988+
modelclass._check_properties(fixed, **kwargs)
879989

880990

881991
def gql(*args, **kwargs):

ndb/tests/unit/test_query.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -881,8 +881,32 @@ def test_OR():
881881
class TestQuery:
882882
@staticmethod
883883
def test_constructor():
884-
with pytest.raises(NotImplementedError):
885-
query.Query()
884+
q = query.Query(kind='Foo')
885+
assert q.kind == 'Foo'
886+
assert q.ancestor == None
887+
assert q.filters == None
888+
assert q.orders == None
889+
890+
@staticmethod
891+
def test_query_errors():
892+
# with pytest.raises(TypeError):
893+
# query.Query(ancestor=
894+
# query.ParameterizedFunction('user', query.Parameter(1)))
895+
with pytest.raises(TypeError):
896+
query.Query(ancestor=42)
897+
# with pytest.raises(ValueError):
898+
# query.Query(ancestor=model.Key('X', None))
899+
# with pytest.raises(TypeError):
900+
# query.Query(ancestor=model.Key('X', 1), app='another')
901+
# with pytest.raises(TypeError):
902+
# query.Query(ancestor=model.Key('X', 1), namespace='another')
903+
with pytest.raises(TypeError):
904+
query.Query(filters=42)
905+
with pytest.raises(TypeError):
906+
query.Query(orders=42)
907+
# with pytest.raises(TypeError):
908+
# query.Query(default_options=42)
909+
886910

887911

888912
def test_gql():

0 commit comments

Comments
 (0)