Skip to content
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

Implement Index Scanning #6008

Merged
merged 12 commits into from
Feb 22, 2022
Merged

Conversation

schmidt-sebastian
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian commented Feb 14, 2022

This adds all index evaluation logic to the Web SDK and ports all tests from Android. The query logic is mostly the same as Android, with the exception of notIn/notEquals queries which are executed by doing scans across split ranges.

Example:

Query q = coll.where("a", ">", 2).where("a", "!=", 3);

Android: SELECT * FROM (SELECT * FROM coll WHERE a > 2) WHERE a != 3

Web: Scan [a>2, a<3] && [a>3, ∞]

@changeset-bot
Copy link

changeset-bot bot commented Feb 14, 2022

⚠️ No Changeset found

Latest commit: 6b88a47

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@schmidt-sebastian schmidt-sebastian force-pushed the mrschmidt/remainingindexmanager branch 2 times, most recently from 36f4a5a to ff719f2 Compare February 14, 2022 23:26
@@ -103,13 +103,12 @@ export interface IndexManager {

/**
* Returns the documents that match the given target based on the provided
* index.
* index. Returns `null` if the target does not have a matching index.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change is pending review on Android: firebase/firebase-android-sdk#3444

}

/** Converts a binary string to a Base64 encoded string. */
export function encodeBase64(raw: string): string {
return new Buffer(raw, 'binary').toString('base64');
return Buffer.from(raw, 'binary').toString('base64');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This creates a deprecation warning in Node.

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Feb 14, 2022

Size Report 1

Affected Products

  • @firebase/firestore

    TypeBase (3a8d4c1)Merge (eaf911b)Diff
    browser244 kB251 kB+6.59 kB (+2.7%)
    esm5304 kB311 kB+7.49 kB (+2.5%)
    main486 kB500 kB+14.0 kB (+2.9%)
    module244 kB251 kB+6.59 kB (+2.7%)
    react-native244 kB251 kB+6.59 kB (+2.7%)
  • @firebase/firestore-lite

    TypeBase (3a8d4c1)Merge (eaf911b)Diff
    browser73.0 kB73.1 kB+137 B (+0.2%)
    esm586.4 kB86.5 kB+137 B (+0.2%)
    main125 kB126 kB+116 B (+0.1%)
    module73.0 kB73.1 kB+137 B (+0.2%)
    react-native73.2 kB73.3 kB+137 B (+0.2%)
  • bundle

    12 size changes

    TypeBase (3a8d4c1)Merge (eaf911b)Diff
    firestore (Persistence)245 kB252 kB+6.72 kB (+2.7%)
    firestore (Query Cursors)192 kB192 kB+140 B (+0.1%)
    firestore (Query)193 kB193 kB+140 B (+0.1%)
    firestore (Read data once)181 kB181 kB+140 B (+0.1%)
    firestore (Realtime updates)183 kB184 kB+140 B (+0.1%)
    firestore (Transaction)166 kB166 kB+140 B (+0.1%)
    firestore (Write data)165 kB165 kB+140 B (+0.1%)
    firestore-lite (Query Cursors)56.8 kB56.9 kB+115 B (+0.2%)
    firestore-lite (Query)59.9 kB60.0 kB+137 B (+0.2%)
    firestore-lite (Read data once)44.4 kB44.5 kB+115 B (+0.3%)
    firestore-lite (Transaction)61.7 kB61.8 kB+115 B (+0.2%)
    firestore-lite (Write data)47.2 kB47.3 kB+115 B (+0.2%)

  • firebase

    TypeBase (3a8d4c1)Merge (eaf911b)Diff
    firebase-compat.js769 kB775 kB+6.57 kB (+0.9%)
    firebase-firestore-compat.js296 kB302 kB+6.57 kB (+2.2%)
    firebase-firestore-lite.js250 kB250 kB+457 B (+0.2%)
    firebase-firestore.js812 kB835 kB+23.2 kB (+2.9%)

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/34LxHt8k9A.html

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Feb 14, 2022

Size Analysis Report 1

This report is too large (651,534 characters) to be displayed here in a GitHub comment. Please use the below link to see the full report on Google Cloud Storage.

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/EYYLLuWl7h.html

Base automatically changed from mrschmidt/updateindexentries to master February 15, 2022 17:02
@schmidt-sebastian schmidt-sebastian force-pushed the mrschmidt/remainingindexmanager branch from d9a242d to 7ccadd8 Compare February 15, 2022 17:09
// that we would split an existing range into multiple ranges that exclude
// the values from any notIn filter.

// The first index range starts with the lower bound and ends at the
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think I understand this well..what if notInValues[0] is not covered by the first range? What if it is < indexRange.lower?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was indeed completely broken, and also did not work correctly on Android. I ported the Android fix firebase/firebase-android-sdk#3454 and adopted it to work with the Web port.

@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Feb 16, 2022
ranges.push(
IDBKeyRange.bound(
indexRange.lower,
this.generateNotInBound(indexRange.lower, barriers[0]),
Copy link
Contributor

Choose a reason for hiding this comment

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

What if barriers[0] is larger than indexRange.upper, is that not possible?

I think we could do something like:

// Assume barriers is sorted, it is right?
let runningLower = indexRange.lower
let runningOpen = indexRange.lowerOpen
for(const barrier: barriers) {
  if(barrier <= indexRange.lower) {continue;}
  if(barrier >= indexRange.upper) {break;}
  ranges.push([runningLower /*need to handle lowerOpen as well*/, barrier]);
  runningLower = barrier
  lowerOpen = true
}

// Handles last
ranges.push([runningLower, indexRange.upper])

You could even do handle this by taking IDBKeyRange[], sort both indexRanges and barriers together..then process from left to right. But maybe let's not go there, it should be rare in real world scenarios.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Barriers are filtered both by the lower and the upper bound, so all barriers should fit between the range.

I have actually spent some time thinking about how to generalize the whole "barrier setup" so that I can treat IDBKeyRanges and barriers the same and only write one loop. I did not consider the approach that you outlined here, which solves some of the problems I had encountered while still reducing the lines of code. I updated the PR accordingly. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Found a bunch of cases where this does not work. Will update soon.

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 updated this code to work on browsers and Node. The browsers seem to do more validations, so I had to change some of the logic here. I also added a bunch of test code. Note that I myself don't fully grasp what is going on here anymore, but the tests are all passing and I think that I covered all corner cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Still broken

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I rewrote this part of the PR in its entirety. It should be much cleaner now.

@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Feb 17, 2022
const bounds: IndexEntry[] = [];
bounds.push(lower);
for (const notInValue of notInValues) {
const sortsAfter = indexEntryComparator(notInValue, lower);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe cmpToLower and cmpToUpper?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good. I am not quite sold on either name, but using "cmp" helps a bit.

const sortsAfter = indexEntryComparator(notInValue, lower);
const sortsBefore = indexEntryComparator(notInValue, upper);

if (sortsAfter > 0 && sortsBefore < 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

How do you feel about:

if (cmpToLower == 0) {
  // Lower happens to be excluded by `notInValue`.
  bounds[0] = lower.successor();
} else if (cmpToLower > 0 && cmpToUpper < 0) {
  // `notInValue` is in the middle of the range
  ...
} else if(cmpToUpper == 0) {
  // Upper happens to be excluded by `notInValue`.
} else {
  // `notInValue` (and hence the following values) is out of the range
  break;
}

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. Good suggestion with the break statement.

* Maps from a target to its equivalent list of sub-targets. Each sub-target
* contains only one term from the target's disjunctive normal form (DNF).
*/
private targetToDnfSubTargets = new ObjectMap<Target, Target[]>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated to this PR: but SubTarget indicates SubQuery as well, and that term meant very different thing to most people.

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'm not quite sure I fully understand this. This part of the code only deals with targets - and only the ones normalized for indexing, not the targets we send to the backend.

Copy link
Contributor

Choose a reason for hiding this comment

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

My point is maybe we can rename the term sub-target..but we can deal with this later.

@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Feb 18, 2022
* Maps from a target to its equivalent list of sub-targets. Each sub-target
* contains only one term from the target's disjunctive normal form (DNF).
*/
private targetToDnfSubTargets = new ObjectMap<Target, Target[]>(
Copy link
Contributor

Choose a reason for hiding this comment

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

My point is maybe we can rename the term sub-target..but we can deal with this later.

const bounds: IndexEntry[] = [];
bounds.push(lower);
for (const notInValue of notInValues) {
const cpmToLower = indexEntryComparator(notInValue, lower);
Copy link
Contributor

Choose a reason for hiding this comment

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

typo

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Feb 22, 2022
@github-actions
Copy link
Contributor

Changeset File Check ⚠️

  • Changeset formatting error in following file:
    Some packages have been changed but no changesets were found. Run `changeset add` to resolve this error.
    If this change doesn't need a release, run `changeset add --empty`.
    

@schmidt-sebastian schmidt-sebastian merged commit 13c0895 into master Feb 22, 2022
@schmidt-sebastian schmidt-sebastian deleted the mrschmidt/remainingindexmanager branch February 22, 2022 23:12
@firebase firebase locked and limited conversation to collaborators Mar 25, 2022
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.

3 participants