From 3b5702819230397eece1056524e80c64fabcacf3 Mon Sep 17 00:00:00 2001 From: Jerjou Cheng Date: Wed, 27 Apr 2016 19:58:35 -0700 Subject: [PATCH] Move in ndb queries code snippets & test. --- appengine/ndb/queries/README.md | 11 + appengine/ndb/queries/guestbook.py | 70 ++++ appengine/ndb/queries/guestbook_test.py | 66 ++++ appengine/ndb/queries/snippets.py | 234 ++++++++++++++ appengine/ndb/queries/snippets_models.py | 65 ++++ appengine/ndb/queries/snippets_test.py | 386 +++++++++++++++++++++++ 6 files changed, 832 insertions(+) create mode 100644 appengine/ndb/queries/README.md create mode 100644 appengine/ndb/queries/guestbook.py create mode 100644 appengine/ndb/queries/guestbook_test.py create mode 100644 appengine/ndb/queries/snippets.py create mode 100644 appengine/ndb/queries/snippets_models.py create mode 100644 appengine/ndb/queries/snippets_test.py diff --git a/appengine/ndb/queries/README.md b/appengine/ndb/queries/README.md new file mode 100644 index 000000000000..067655c6674f --- /dev/null +++ b/appengine/ndb/queries/README.md @@ -0,0 +1,11 @@ +## App Engine Datastore NDB Queries Samples + +This contains snippets used in the NDB queries documentation, demonstrating +various ways to make ndb queries. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/ndb/queries + + diff --git a/appengine/ndb/queries/guestbook.py b/appengine/ndb/queries/guestbook.py new file mode 100644 index 000000000000..767379433cc6 --- /dev/null +++ b/appengine/ndb/queries/guestbook.py @@ -0,0 +1,70 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cgi + +from google.appengine.datastore.datastore_query import Cursor +from google.appengine.ext import ndb +import webapp2 + + +class Greeting(ndb.Model): + '''Models an individual Guestbook entry with content and date.''' + content = ndb.StringProperty() + date = ndb.DateTimeProperty(auto_now_add=True) + + @classmethod + def query_book(cls, ancestor_key): + return cls.query(ancestor=ancestor_key).order(-cls.date) + + +class MainPage(webapp2.RequestHandler): + def get(self): + guestbook_name = self.request.get('guestbook_name') + ancestor_key = ndb.Key('Book', guestbook_name or '*notitle*') + greetings = Greeting.query_book(ancestor_key).fetch(20) + + self.response.out.write('') + + for greeting in greetings: + self.response.out.write( + '
%s
' % cgi.escape(greeting.content)) + + self.response.out.write('') + + +class List(webapp2.RequestHandler): + def get(self): + """Handles requests like /list?cursor=1234567.""" + cursor = Cursor(urlsafe=self.request.get('cursor')) + greets, next_cursor, more = Greeting.query().fetch_page( + 10, start_cursor=cursor) + + self.response.out.write('') + + for greeting in greets: + self.response.out.write( + '
%s
' % cgi.escape(greeting.content)) + + if more and next_cursor: + self.response.out.write('More...' % + next_cursor.urlsafe()) + + self.response.out.write('') + + +app = webapp2.WSGIApplication([ + ('/', MainPage), + ('/list', List), +], debug=True) diff --git a/appengine/ndb/queries/guestbook_test.py b/appengine/ndb/queries/guestbook_test.py new file mode 100644 index 000000000000..d9da413a8d54 --- /dev/null +++ b/appengine/ndb/queries/guestbook_test.py @@ -0,0 +1,66 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +from google.appengine.ext import ndb +import guestbook +import pytest +import webtest + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(guestbook.app) + + +def test_main(app): + # Add a greeting to find + guestbook.Greeting( + content='Hello world', + parent=ndb.Key('Book', 'brane3')).put() + + # Add a greeting to not find. + guestbook.Greeting( + content='Flat sheet', + parent=ndb.Key('Book', 'brane2')).put() + + response = app.get('/?guestbook_name=brane3') + + assert response.status_int == 200 + assert 'Hello world' in response.body + assert 'Flat sheet' not in response.body + + +def test_list(app): + # Add greetings to find + for i in range(11): + guestbook.Greeting(content='Greeting {}'.format(i)).put() + + response = app.get('/list') + assert response.status_int == 200 + + assert 'Greeting 0' in response.body + assert 'Greeting 9' in response.body + assert 'Greeting 10' not in response.body + + next_page = re.search(r'href="([^"]+)"', response.body).group(1) + assert next_page is not None + + response = app.get(next_page) + assert response.status_int == 200 + + assert 'Greeting 0' not in response.body + assert 'Greeting 10' in response.body + assert 'More' not in response.body diff --git a/appengine/ndb/queries/snippets.py b/appengine/ndb/queries/snippets.py new file mode 100644 index 000000000000..30a364f7ab65 --- /dev/null +++ b/appengine/ndb/queries/snippets.py @@ -0,0 +1,234 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb +from guestbook import Greeting +from snippets_models import * # noqa + + +def query_account_equality(): + query = Account.query(Account.userid == 42) + return query + + +def query_account_inequality(): + query = Account.query(Account.userid >= 40) + return query + + +def query_account_multiple_filters(): + query = Account.query(Account.userid >= 40, Account.userid < 50) + return query + + +def query_account_in_steps(): + query1 = Account.query() # Retrieve all Account entitites + query2 = query1.filter(Account.userid >= 40) # Filter on userid >= 40 + query3 = query2.filter(Account.userid < 50) # Filter on userid < 50 too + return query1, query2, query3 + + +def query_article_inequality(): + query = Article.query(Article.tags != 'perl') + return query + + +def query_article_inequality_explicit(): + query = Article.query(ndb.OR(Article.tags < 'perl', + Article.tags > 'perl')) + return query + + +def articles_with_tags_example(): + # [START included_in_inequality] + Article(title='Perl + Python = Parrot', + stars=5, + tags=['python', 'perl']) + # [END included_in_inequality] + # [START excluded_from_inequality] + Article(title='Introduction to Perl', + stars=3, + tags=['perl']) + # [END excluded_from_inequality] + + +def query_article_in(): + query = Article.query(Article.tags.IN(['python', 'ruby', 'php'])) + return query + + +def query_article_in_equivalent(): + query = Article.query(ndb.OR(Article.tags == 'python', + Article.tags == 'ruby', + Article.tags == 'php')) + return query + + +def query_article_nested(): + query = Article.query(ndb.AND(Article.tags == 'python', + ndb.OR(Article.tags.IN(['ruby', 'jruby']), + ndb.AND(Article.tags == 'php', + Article.tags != 'perl')))) + return query + + +def query_greeting_order(): + query = Greeting.query().order(Greeting.content, -Greeting.date) + return query + + +def query_greeting_multiple_orders(): + query = Greeting.query().order(Greeting.content).order(-Greeting.date) + return query + + +def query_purchase_with_customer_key(): + # [START purchase_with_customer_key_models] + class Customer(ndb.Model): + name = ndb.StringProperty() + + class Purchase(ndb.Model): + customer = ndb.KeyProperty(kind=Customer) + price = ndb.IntegerProperty() + # [END purchase_with_customer_key_models] + + def query_purchases_for_customer_via_key(customer_entity): + purchases = Purchase.query( + Purchase.customer == customer_entity.key).fetch() + return purchases + + return Customer, Purchase, query_purchases_for_customer_via_key + + +def query_purchase_with_ancestor_key(): + # [START purchase_with_ancestor_key_models] + class Customer(ndb.Model): + name = ndb.StringProperty() + + class Purchase(ndb.Model): + price = ndb.IntegerProperty() + # [END purchase_with_ancestor_key_models] + + def create_purchase_for_customer_with_ancestor(customer_entity): + purchase = Purchase(parent=customer_entity.key) + return purchase + + def query_for_purchases_of_customer_with_ancestor(customer_entity): + purchases = Purchase.query(ancestor=customer_entity.key).fetch() + return purchases + + return (Customer, Purchase, + create_purchase_for_customer_with_ancestor, + query_for_purchases_of_customer_with_ancestor) + + +def print_query(): + print(Employee.query()) + # -> Query(kind='Employee') + print(Employee.query(ancestor=ndb.Key(Manager, 1))) + # -> Query(kind='Employee', ancestor=Key('Manager', 1)) + + +def query_contact_with_city(): + query = Contact.query(Contact.addresses.city == 'Amsterdam') + return query + + +def query_contact_sub_entities_beware(): + query = Contact.query(Contact.addresses.city == 'Amsterdam', # Beware! + Contact.addresses.street == 'Spear St') + return query + + +def query_contact_multiple_values_in_single_sub_entity(): + query = Contact.query(Contact.addresses == Address(city='San Francisco', + street='Spear St')) + return query + + +def query_properties_named_by_string_on_expando(): + property_to_query = 'location' + query = FlexEmployee.query(ndb.GenericProperty(property_to_query) == 'SF') + return query + + +def query_properties_named_by_string_for_defined_properties(keyword, value): + query = Article.query(Article._properties[keyword] == value) + return query + + +def query_properties_named_by_string_using_getattr(keyword, value): + query = Article.query(getattr(Article, keyword) == value) + return query + + +def order_query_results_by_property(keyword): + expando_query = FlexEmployee.query().order(ndb.GenericProperty('location')) + + property_query = Article.query().order(Article._properties[keyword]) + + return expando_query, property_query + + +def print_query_keys(query): + for key in query.iter(keys_only=True): + print(key) + + +def reverse_queries(): + # Set up. + q = Bar.query() + q_forward = q.order(Bar.key) + q_reverse = q.order(-Bar.key) + + # Fetch a page going forward. + bars, cursor, more = q_forward.fetch_page(10) + + # Fetch the same page going backward. + r_bars, r_cursor, r_more = q_reverse.fetch_page(10, start_cursor=cursor) + + return (bars, cursor, more), (r_bars, r_cursor, r_more) + + +def fetch_message_accounts_inefficient(message_query): + message_account_pairs = [] + for message in message_query: + key = ndb.Key('Account', message.userid) + account = key.get() + message_account_pairs.append((message, account)) + + return message_account_pairs + + +def fetch_message_accounts_efficient(message_query): + def callback(message): + key = ndb.Key('Account', message.userid) + account = key.get() + return message, account + + message_account_pairs = message_query.map(callback) + # Now message_account_pairs is a list of (message, account) tuples. + return message_account_pairs + + +def query_gql_long(): + query = ndb.gql("SELECT * FROM Article WHERE stars > :1") + query2 = query.bind(3) + + return query, query2 + + +def query_gql_short(): + query = ndb.gql("SELECT * FROM Article WHERE stars > :1", 3) + return query diff --git a/appengine/ndb/queries/snippets_models.py b/appengine/ndb/queries/snippets_models.py new file mode 100644 index 000000000000..be497850e34a --- /dev/null +++ b/appengine/ndb/queries/snippets_models.py @@ -0,0 +1,65 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + + +class Account(ndb.Model): + username = ndb.StringProperty() + userid = ndb.IntegerProperty() + email = ndb.StringProperty() + + +class Address(ndb.Model): + type = ndb.StringProperty() # E.g., 'home', 'work' + street = ndb.StringProperty() + city = ndb.StringProperty() + + +class Contact(ndb.Model): + name = ndb.StringProperty() + addresses = ndb.StructuredProperty(Address, repeated=True) + + +class Article(ndb.Model): + title = ndb.StringProperty() + stars = ndb.IntegerProperty() + tags = ndb.StringProperty(repeated=True) + + +class ArticleWithDifferentDatastoreName(ndb.Model): + title = ndb.StringProperty('t') + + +class Employee(ndb.Model): + full_name = ndb.StringProperty('n') + retirement_age = ndb.IntegerProperty('r') + + +class Manager(ndb.Model): + pass + + +class FlexEmployee(ndb.Expando): + name = ndb.StringProperty() + age = ndb.IntegerProperty() + + +class Bar(ndb.Model): + pass + + +class Message(ndb.Model): + content = ndb.StringProperty() + userid = ndb.IntegerProperty() diff --git a/appengine/ndb/queries/snippets_test.py b/appengine/ndb/queries/snippets_test.py new file mode 100644 index 000000000000..7764e2f89134 --- /dev/null +++ b/appengine/ndb/queries/snippets_test.py @@ -0,0 +1,386 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from guestbook import Greeting +import snippets +from snippets_models import * # noqa + + +def test_query_account_equality(testbed): + Account(userid=42).put() + Account(userid=43).put() + + query = snippets.query_account_equality() + accounts = query.fetch() + + assert len(accounts) == 1 + assert accounts[0].userid == 42 + + +def test_query_account_inequality(testbed): + Account(userid=32).put() + Account(userid=42).put() + Account(userid=43).put() + + query = snippets.query_account_inequality() + accounts = query.fetch() + + assert len(accounts) == 2 + assert all(a.userid > 40 for a in accounts) + + +def test_query_account_multiple_filters(testbed): + Account(userid=40).put() + Account(userid=49).put() + Account(userid=50).put() + Account(userid=6).put() + Account(userid=62).put() + + query = snippets.query_account_multiple_filters() + accounts = query.fetch() + + assert len(accounts) == 2 + assert all(40 <= a.userid < 50 for a in accounts) + + +def test_query_account_in_steps(testbed): + Account(userid=40).put() + Account(userid=49).put() + Account(userid=50).put() + Account(userid=6).put() + Account(userid=62).put() + + _, _, query = snippets.query_account_in_steps() + accounts = query.fetch() + + assert len(accounts) == 2 + assert all(40 <= a.userid < 50 for a in accounts) + + +def test_query_article_inequality(testbed): + Article(tags=['perl']).put() + Article(tags=['perl', 'python']).put() + + query = snippets.query_article_inequality() + articles = query.fetch() + + assert len(articles) == 1 + + +def test_query_article_inequality_explicit(testbed): + Article(tags=['perl']).put() + Article(tags=['perl', 'python']).put() + + query = snippets.query_article_inequality_explicit() + articles = query.fetch() + + assert len(articles) == 1 + + +def test_articles_with_tags_example(testbed): + snippets.articles_with_tags_example() + + +def test_query_article_in(testbed): + Article(tags=['perl']).put() + Article(tags=['perl', 'python']).put() + Article(tags=['ruby']).put() + Article(tags=['php']).put() + + query = snippets.query_article_in() + articles = query.fetch() + + assert len(articles) == 3 + + +def test_query_article_in_equivalent(testbed): + Article(tags=['perl']).put() + Article(tags=['perl', 'python']).put() + Article(tags=['ruby']).put() + Article(tags=['php']).put() + + query = snippets.query_article_in_equivalent() + articles = query.fetch() + + assert len(articles) == 3 + + +def test_query_article_nested(testbed): + Article(tags=['python']).put() # excluded - no non-python + Article(tags=['ruby']).put() # excluded - no python + Article(tags=['python', 'ruby']).put() # included + Article(tags=['python', 'jruby']).put() # included + Article(tags=['python', 'ruby', 'jruby']).put() # included + Article(tags=['python', 'php']).put() # included + Article(tags=['python', 'perl']).put() # excluded + + query = snippets.query_article_nested() + articles = query.fetch() + assert len(articles) == 4 + + +def test_query_greeting_order(testbed): + Greeting(content='3').put() + Greeting(content='2').put() + Greeting(content='1').put() + Greeting(content='2').put() + + query = snippets.query_greeting_order() + greetings = query.fetch() + + assert (greetings[0].content < greetings[1].content < greetings[3].content) + assert greetings[1].content == greetings[2].content + assert greetings[1].date > greetings[2].date + + +def test_query_greeting_multiple_orders(testbed): + Greeting(content='3').put() + Greeting(content='2').put() + Greeting(content='1').put() + Greeting(content='2').put() + + query = snippets.query_greeting_multiple_orders() + greetings = query.fetch() + + assert (greetings[0].content < greetings[1].content < greetings[3].content) + assert greetings[1].content == greetings[2].content + assert greetings[1].date > greetings[2].date + + +def test_query_purchase_with_customer_key(testbed): + Customer, Purchase, do_query = snippets.query_purchase_with_customer_key() + charles = Customer(name='Charles') + charles.put() + snoop_key = Customer(name='Snoop').put() + Purchase(price=123, customer=charles.key).put() + Purchase(price=234, customer=snoop_key).put() + + purchases = do_query(charles) + assert len(purchases) == 1 + assert purchases[0].price == 123 + + +def test_query_purchase_with_ancestor_key(testbed): + Customer, Purchase, create_purchase, do_query = ( + snippets.query_purchase_with_ancestor_key()) + charles = Customer(name='Charles') + charles.put() + snoop = Customer(name='Snoop') + snoop.put() + + charles_purchase = create_purchase(charles) + charles_purchase.price = 123 + charles_purchase.put() + + snoop_purchase = create_purchase(snoop) + snoop_purchase.price = 234 + snoop_purchase.put() + + purchases = do_query(snoop) + assert len(purchases) == 1 + assert purchases[0].price == 234 + + +def test_print_query(testbed, capsys): + snippets.print_query() + stdout, _ = capsys.readouterr() + + assert '' in stdout + + +def test_query_contact_with_city(testbed): + address = Address(type='home', street='Spear St', city='Amsterdam') + address.put() + Contact(name='Bertus Aafjes', addresses=[address]).put() + address1 = Address(type='home', street='Damrak St', city='Amsterdam') + address1.put() + address2 = Address(type='work', street='Spear St', city='San Francisco') + address2.put() + Contact(name='Willem Jan Aalders', addresses=[address1, address2]).put() + address = Address(type='home', street='29th St', city='San Francisco') + address.put() + Contact(name='Hans Aarsman', addresses=[address]).put() + + query = snippets.query_contact_with_city() + contacts = query.fetch() + + assert len(contacts) == 2 + + +def test_query_contact_sub_entities_beware(testbed): + address = Address(type='home', street='Spear St', city='Amsterdam') + address.put() + Contact(name='Bertus Aafjes', addresses=[address]).put() + address1 = Address(type='home', street='Damrak St', city='Amsterdam') + address1.put() + address2 = Address(type='work', street='Spear St', city='San Francisco') + address2.put() + Contact(name='Willem Jan Aalders', addresses=[address1, address2]).put() + address = Address(type='home', street='29th St', city='San Francisco') + address.put() + Contact(name='Hans Aarsman', addresses=[address]).put() + + query = snippets.query_contact_sub_entities_beware() + contacts = query.fetch() + + assert len(contacts) == 2 + for contact in contacts: + assert ('Spear St' in [a.street for a in contact.addresses] or + 'Amsterdam' in [a.city for a in contact.addresses]) + + +def test_query_contact_multiple_values_in_single_sub_entity(testbed): + address = Address(type='home', street='Spear St', city='Amsterdam') + address.put() + Contact(name='Bertus Aafjes', addresses=[address]).put() + address1 = Address(type='home', street='Damrak St', city='Amsterdam') + address1.put() + address2 = Address(type='work', street='Spear St', city='San Francisco') + address2.put() + Contact(name='Willem Jan Aalders', addresses=[address1, address2]).put() + address = Address(type='home', street='29th St', city='San Francisco') + address.put() + Contact(name='Hans Aarsman', addresses=[address]).put() + + query = snippets.query_contact_multiple_values_in_single_sub_entity() + contacts = query.fetch() + + assert len(contacts) == 1 + assert any(a.city == 'San Francisco' and a.street == 'Spear St' + for a in contacts[0].addresses) + + +def test_query_properties_named_by_string_on_expando(testbed): + FlexEmployee(location='SF').put() + FlexEmployee(location='Amsterdam').put() + + query = snippets.query_properties_named_by_string_on_expando() + employees = query.fetch() + assert len(employees) == 1 + + +def test_query_properties_named_by_string_for_defined_properties(testbed): + Article(title='from').put() + Article(title='to').put() + + query = snippets.query_properties_named_by_string_for_defined_properties( + 'title', 'from') + articles = query.fetch() + + assert len(articles) == 1 + + +def test_query_properties_named_by_string_using_getattr(testbed): + Article(title='from').put() + Article(title='to').put() + + query = snippets.query_properties_named_by_string_using_getattr( + 'title', 'from') + articles = query.fetch() + + assert len(articles) == 1 + + +def test_order_query_results_by_property(testbed): + Article(title='2').put() + Article(title='1').put() + FlexEmployee(location=2).put() + FlexEmployee(location=1).put() + expando_query, property_query = snippets.order_query_results_by_property( + 'title') + + assert expando_query.fetch()[0].location == 1 + assert property_query.fetch()[0].title == '1' + + +def test_print_query_keys(testbed, capsys): + for i in range(3): + Article(title='title {}'.format(i)).put() + + snippets.print_query_keys(Article.query()) + + stdout, _ = capsys.readouterr() + assert "Key('Article'" in stdout + + +def test_reverse_queries(testbed): + for i in range(11): + Bar().put() + + (bars, cursor, more), (r_bars, r_cursor, r_more) = ( + snippets.reverse_queries()) + + assert len(bars) == 10 + assert len(r_bars) == 10 + + for prev_bar, bar in zip(bars, bars[1:]): + assert prev_bar.key < bar.key + + for prev_bar, bar in zip(r_bars, r_bars[1:]): + assert prev_bar.key > bar.key + + +def test_fetch_message_accounts_inefficient(testbed): + for i in range(1, 6): + Account(username='Account %s' % i, id=i).put() + Message(content='Message %s' % i, userid=i).put() + + message_account_pairs = snippets.fetch_message_accounts_inefficient( + Message.query().order(Message.userid)) + + assert len(message_account_pairs) == 5 + + print repr(message_account_pairs) + for i in range(1, 6): + message, account = message_account_pairs[i - 1] + assert message.content == 'Message %s' % i + assert account.username == 'Account %s' % i + + +def test_fetch_message_accounts_efficient(testbed): + for i in range(1, 6): + Account(username='Account %s' % i, id=i).put() + Message(content='Message %s' % i, userid=i).put() + + message_account_pairs = snippets.fetch_message_accounts_efficient( + Message.query().order(Message.userid)) + + assert len(message_account_pairs) == 5 + + for i in range(1, 6): + message, account = message_account_pairs[i - 1] + assert message.content == 'Message %s' % i + assert account.username == 'Account %s' % i + + +def test_query_gql_long(testbed): + for i in range(1, 6): + Article(stars=i).put() + + query, query2 = snippets.query_gql_long() + articles = query2.fetch() + + assert len(articles) == 2 + assert all(a.stars > 3 for a in articles) + + +def test_query_gql_short(testbed): + for i in range(1, 6): + Article(stars=i).put() + + query = snippets.query_gql_short() + articles = query.fetch() + + assert len(articles) == 2 + assert all(a.stars > 3 for a in articles)