Skip to content

[2.8/3] Add limitToLast implementation and tests. #2296

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

wu-hui
Copy link
Contributor

@wu-hui wu-hui commented Oct 24, 2019

Spec tests are not in this PR, as I need to basically implement the target:query split and mapping in spec tests builder/runner.

Figured it might be a good idea to have it reviewed without spec tests. Please let me know if you want them to be together.

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this! This looks pretty reasonable. I added some comments for cleanup, but I do think this is close. I am excited to see the multi-tab spec tests in the following PR, but the test coverage here makes me feel confident. Thank you also for cleaning up the comments/code in Index Free.

@@ -8079,14 +8079,23 @@ declare namespace firebase.firestore {
): Query;

/**
* Creates and returns a new Query where the results are limited to the
* specified number of documents.
* Creates and returns a new Query where only the first matching documents
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of making this slightly more concise?

"Creates and returns a new Query that only returns the first matching documents".

I tried to come up with a way to include the original "specified number of documents" phrasing, but I can't come up with a good idea short of: "the first n matching documents". However, this would require us to rename the argument. Maybe you have an idea on how we can combine "first" and "specified"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your version is good enough.

* Creates and returns a new Query where the results are limited to the
* specified number of documents.
* Creates and returns a new Query where only the first matching documents
* are returned as results.
*
* @param limit The maximum number of items to return.
* @return The created Query.
*/
limit(limit: number): Query;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original API proposal suggests to make this method as deprecated. Should we do that and introduce limitToFirst()? In that case, I would also leave the comments for limit as they were.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the decision was to keep limit and add limitToLast only.

See the comments in go/limitToLast

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Do you mind updating the proposed API so this is immediately apparent? The API proposal should be the source of truth and we should in general update the proposal to match the decision from the API council.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made some edit suggestions there.

@@ -1091,6 +1091,15 @@ export class Query {
*/
limit(limit: number): Query;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure that both API definitions/comments are in sync (they seemed to be slightly different even before this PR).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -1539,14 +1539,24 @@ export class Query implements firestore.Query {
limit(n: number): firestore.Query {
validateExactNumberOfArgs('Query.limit', arguments, 1);
validateArgType('Query.limit', 'number', 1, n);
this.verifyPositiveLimit(n);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of moving this to util/input_validation, which means that we could make this generic and include the callsite (Query.limitToFirst, Query.limitToLast)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

get(options?: firestore.GetOptions): Promise<firestore.QuerySnapshot> {
validateBetweenNumberOfArgs('Query.get', arguments, 0, 1);
validateGetOptions('Query.get', options);
return new Promise(
(resolve: Resolver<firestore.QuerySnapshot>, reject: Rejecter) => {
this.validateHasExplicitOrderByForLimitToLast(this._query);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validation should happen outside of the Promise. Validation errors should throw, but if you throw inside of a Promise this is converted to a rejected Promise (which would indicate a backend error).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

});
});

it('can unlisten to mirror queries', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/to/from

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

{ k: 'a', sort: 0 },
{ k: 'e', sort: -1 }
]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test might be easier to reason about if you remove the expect(toDataArray(snapshot)) asserts that you have already verified in the test above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this assertion is beneficial because it checks unlistening from one of the mirror queries will not stop others from functioning.

Added a comment to make the intention more clear.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there maybe another way to reduce the test case size? 40 lines of shared setup seems a little much in my personal opinion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed some unnecessary asserts that are repeating what other tests already covered, this should help a little bit?

I understand you concern, but IMHO it's not worth to make them sharing setup in this case, the added abstraction would probably make these tests harder to read.

snapshot = await storeLimitToLastEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'e', sort: -1 },
{ k: 'a', sort: -2 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above - the test would be more concise if it focused on relistening, rather than verifying each individual result. If you can keep the same test coverage, it might be worth removing some of the duplicated asserts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment like above.

@@ -264,7 +264,28 @@ describe('IndexFreeQueryEngine', () => {
it('does not use initial results for limit query with document removal', async () => {
const query = Query.atPath(path('coll'))
.addFilter(filter('matches', '==', true))
.withLimit(1);
.withLimitToFirst(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of duplicating the limit() tests, what do you think about making one of them use limitToLast() and keeping just limitToFirst() for the rest of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

fromLimit.toTargetQuery().canonicalId()
);

expect(limitQuery.isEqual(fromLimitToLast.toTargetQuery())).to.be.true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still concerned about this method. The contract just doesn't feel right and seeing this spelled out in a test makes this obvious. I understand that there may be a need for this conversion, but since this is limited to Multi-Tab and Web-only, what to you think of handling this conversion in a private method in Sync Engine (or a free-standing function):

function convertTargetToMultiTabQuery(...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point that this is multi-tab only. Moved it to SyncEngine.

Copy link
Contributor Author

@wu-hui wu-hui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for make some time to review this!

@@ -8079,14 +8079,23 @@ declare namespace firebase.firestore {
): Query;

/**
* Creates and returns a new Query where the results are limited to the
* specified number of documents.
* Creates and returns a new Query where only the first matching documents
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your version is good enough.

* Creates and returns a new Query where the results are limited to the
* specified number of documents.
* Creates and returns a new Query where only the first matching documents
* are returned as results.
*
* @param limit The maximum number of items to return.
* @return The created Query.
*/
limit(limit: number): Query;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the decision was to keep limit and add limitToLast only.

See the comments in go/limitToLast

* Creates and returns a new Query where only the last matching documents
* are returned as results.
*
* @param limit The maximum number of items to return.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -1091,6 +1091,15 @@ export class Query {
*/
limit(limit: number): Query;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -1539,14 +1539,24 @@ export class Query implements firestore.Query {
limit(n: number): firestore.Query {
validateExactNumberOfArgs('Query.limit', arguments, 1);
validateArgType('Query.limit', 'number', 1, n);
this.verifyPositiveLimit(n);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

});
});

it('can unlisten to mirror queries', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

{ k: 'a', sort: 0 },
{ k: 'e', sort: -1 }
]);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this assertion is beneficial because it checks unlistening from one of the mirror queries will not stop others from functioning.

Added a comment to make the intention more clear.

snapshot = await storeLimitToLastEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'e', sort: -1 },
{ k: 'a', sort: -2 }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment like above.

@@ -264,7 +264,28 @@ describe('IndexFreeQueryEngine', () => {
it('does not use initial results for limit query with document removal', async () => {
const query = Query.atPath(path('coll'))
.addFilter(filter('matches', '==', true))
.withLimit(1);
.withLimitToFirst(1);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

fromLimit.toTargetQuery().canonicalId()
);

expect(limitQuery.isEqual(fromLimitToLast.toTargetQuery())).to.be.true;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point that this is multi-tab only. Moved it to SyncEngine.

@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Oct 27, 2019
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. Can you see if you can come up with a strategy to remove the test duplication? Ideally, our GZIP compression rate for our tests would be pretty low :-P

* Creates and returns a new Query where the results are limited to the
* specified number of documents.
* Creates and returns a new Query where only the first matching documents
* are returned as results.
*
* @param limit The maximum number of items to return.
* @return The created Query.
*/
limit(limit: number): Query;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Do you mind updating the proposed API so this is immediately apparent? The API proposal should be the source of truth and we should in general update the proposal to match the decision from the API council.

* Creates and returns a new Query where only the last matching documents
* are returned as results.
*
* @param limit The maximum number of items to return.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

You must specify at least one `orderBy` clause for `limitToLast` queries, otherwise an exception will be thrown thrown during execution.

If you like your version better, we should still make it "an exception will be thrown".

@@ -1861,11 +1872,21 @@ export class Query implements firestore.Query {
};
}

private validateHasExplicitOrderByForLimitToLast(query: InternalQuery): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per our standup discussion, let's stay consistent with the rest of the Query methods (and explore the option to allow startAt().order()). Let's figure out what is feasible and hopefully we can leave this PR as is.

* different from the original `Query` from which we obtained this instance.
*/
// PORTING NOTE: Multi-tab only
private targetToQuery(target: Target): Query {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to indicate that this conversion is lossy? We use "synthesized" in other parts of the API already 9

static createSynthesizedRemoteEventForCurrentChange(
), maybe this could be targetToSynthesizedQuery or synthesizeTargetToQuery?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

!objUtils.contains(this.listenTargets, queryData.targetId),
'listen called with duplicate targetId!'
);
if (objUtils.contains(this.listenTargets, queryData.targetId)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. As per standup discussion, let's leave this as is.

if (n <= 0) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
`Function "${functionName}" created invalid Query. Query limit (${n}) is invalid. Limit must be positive.`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should now be generic:

export function validatePositiveNumber(functionName: string, position: number, n: number): void {
  if (n <= 0) {
    const description = valueDescription(argument); 
    throw new FirestoreError(
      Code.INVALID_ARGUMENT,
      `Function "${functionName}()" requires its ${ordinal(position)} argument to be a positive number, but it was: ${description}`);
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

});

it('can listen to mirror queries', () => {
const testDocs = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about something like:

Queries that mapped to the same target ID are referred to as "mirror queries". An example for a mirror query is a limitToLast() query and a limit() query that share the same backend Target ID. Since limitToLast() queries are sent to the backend with a modified orderBy() clause, they can map to the same target representation as limit() query, even if both queries appear separate to the user.

{ k: 'a', sort: 0 },
{ k: 'e', sort: -1 }
]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there maybe another way to reduce the test case size? 40 lines of shared setup seems a little much in my personal opinion.

@schmidt-sebastian schmidt-sebastian removed their assignment Oct 28, 2019
@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Oct 29, 2019
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the feedback. Some small cleanup left, but ship it :)

@@ -1540,14 +1540,14 @@ export class Query implements firestore.Query {
limit(n: number): firestore.Query {
validateExactNumberOfArgs('Query.limit', arguments, 1);
validateArgType('Query.limit', 'number', 1, n);
validatePositiveLimit('limit', n);
validatePositiveNumber('limit', 1, n);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name here should be "Query.limit" (here and below).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -1008,11 +1008,12 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
return activeQueries;
}
/**
* Creates a `Query` object from this `Target`. Note the result might be
* different from the original `Query` from which we obtained this instance.
* Creates a `Query` object from this `Target`. There is no way to obtain
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/this/the specified

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* different from the original `Query` from which we obtained this instance.
* Creates a `Query` object from this `Target`. There is no way to obtain
* the original `Query`, so we synthesize a `Query` from the `Target` object.
* The result might be different from the original `Query`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace comma with period. Maybe also mention that is used for multi-tab and hence the limitation described here does not alter the behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -140,8 +140,12 @@ apiDescribe('Queries', (persistence: boolean) => {
});
});

// Mirror queries are `Query`s mapped to the same `Target`, meaning
// they are supported by the same backend Query.
// Queries that mapped to the same target ID are referred to as
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/mapped/map (I take credit for that one)

Also maybe start with "Two queries...".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor Author

@wu-hui wu-hui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the help!

@@ -1540,14 +1540,14 @@ export class Query implements firestore.Query {
limit(n: number): firestore.Query {
validateExactNumberOfArgs('Query.limit', arguments, 1);
validateArgType('Query.limit', 'number', 1, n);
validatePositiveLimit('limit', n);
validatePositiveNumber('limit', 1, n);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -1861,11 +1872,21 @@ export class Query implements firestore.Query {
};
}

private validateHasExplicitOrderByForLimitToLast(query: InternalQuery): void {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK.

@@ -1008,11 +1008,12 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
return activeQueries;
}
/**
* Creates a `Query` object from this `Target`. Note the result might be
* different from the original `Query` from which we obtained this instance.
* Creates a `Query` object from this `Target`. There is no way to obtain
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* different from the original `Query` from which we obtained this instance.
* Creates a `Query` object from this `Target`. There is no way to obtain
* the original `Query`, so we synthesize a `Query` from the `Target` object.
* The result might be different from the original `Query`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -140,8 +140,12 @@ apiDescribe('Queries', (persistence: boolean) => {
});
});

// Mirror queries are `Query`s mapped to the same `Target`, meaning
// they are supported by the same backend Query.
// Queries that mapped to the same target ID are referred to as
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@wu-hui wu-hui merged commit aa9390e into wuandy/QueryTargetAndLimitToLast Oct 29, 2019
wu-hui added a commit that referenced this pull request Nov 2, 2019
* [1/3] Query/Target split -- create Target and make remote/ and local/ work with Target. (#2254)

* [2/3] Query/Target split: make SyncEngine able to handle the mapping between Target and Query. (#2281)

* [2.8/3] Add limitToLast implementation and tests. (#2296)

* [3/3] Spec tests for limitToLast+multi-tab (#2300)

* Add spec test for single client (#2313)

* add change log for limitToLast

* Address comments
@firebase firebase locked and limited conversation to collaborators Nov 29, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants