Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/firestore/__tests__/firestore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ import firestore, {
enablePersistentCacheIndexAutoCreation,
onSnapshotsInSync,
documentId,
vector,
VectorValue,
} from '../lib';

const COLLECTION = 'firestore';
Expand Down Expand Up @@ -746,6 +748,14 @@ describe('Firestore', function () {
it('`documentId` is properly exposed to end user', function () {
expect(documentId).toBeDefined();
});

it('`VectorValue` is properly exposed to end user', function () {
expect(VectorValue).toBeDefined();
});

it('`vector()` is properly exposed to end user', function () {
expect(vector).toBeDefined();
});
});

describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () {
Expand Down Expand Up @@ -1437,4 +1447,37 @@ describe('Firestore', function () {
});
});
});

describe('VectorValue (unit serializer)', function () {
it('constructs and validates values', function () {
const v = vector([0, 1.5, -2]);
expect(v.toArray()).toEqual([0, 1.5, -2]);
expect(v.isEqual(vector([0, 1.5, -2]))).toBe(true);
expect(v.isEqual(vector([0, 1.5]))).toBe(false);
});

it('serializes to type map and parses back', function () {
const serialize = require('../lib/utils/serialize');
const { getTypeMapName } = require('../lib/utils/typemap');

const v = vector([0.1, 0.2, 0.3]);
const typed = serialize.generateNativeData(v, false);
expect(Array.isArray(typed)).toBe(true);
expect(getTypeMapName(typed[0])).toBe('vector');
const parsed = serialize.parseNativeData(null, typed);
expect(parsed.toArray()).toEqual([0.1, 0.2, 0.3]);
});

it('serializes inside objects and arrays', function () {
const serialize = require('../lib/utils/serialize');
const { getTypeMapName } = require('../lib/utils/typemap');

const v = vector([1, 2, 3]);
const map = serialize.buildNativeMap({ a: v }, false);
expect(getTypeMapName(map.a[0])).toBe('vector');

const arr = serialize.buildNativeArray([v], false);
expect(getTypeMapName(arr[0][0])).toBe('vector');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.google.firebase.firestore.MetadataChanges;
import com.google.firebase.firestore.QuerySnapshot;
import com.google.firebase.firestore.SnapshotMetadata;
import com.google.firebase.firestore.VectorValue;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -71,6 +72,7 @@ public class ReactNativeFirebaseFirestoreSerialize {
private static final int INT_OBJECT = 16;
private static final int INT_INTEGER = 17;
private static final int INT_NEGATIVE_ZERO = 18;
private static final int INT_VECTOR = 19;
private static final int INT_UNKNOWN = -999;

// Keys
Expand Down Expand Up @@ -404,6 +406,12 @@ private static WritableArray buildTypeMap(Object value) {
return typeArray;
}

if (value instanceof VectorValue) {
typeArray.pushInt(INT_VECTOR);
typeArray.pushArray(Arguments.fromArray(((VectorValue) value).toArray()));
return typeArray;
}

Log.w(TAG, "Unknown object of type " + value.getClass());

typeArray.pushInt(INT_UNKNOWN);
Expand Down Expand Up @@ -520,6 +528,12 @@ static Object parseTypeMap(FirebaseFirestore firestore, ReadableArray typeArray)
}
case INT_OBJECT:
return parseReadableMap(firestore, typeArray.getMap(1));
case INT_VECTOR:
ReadableArray vals = typeArray.getArray(1);
int length = vals != null ? vals.size() : 0;
double[] doubles = new double[length];
for (int i = 0; i < length; i++) doubles[i] = vals.getDouble(i);
return FieldValue.vector(doubles);
case INT_UNKNOWN:
default:
return null;
Expand Down
84 changes: 84 additions & 0 deletions packages/firestore/e2e/VectorValue.e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library 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.
*
*/

const COLLECTION = 'firestore';

describe('firestore.VectorValue', function () {
describe('modular', function () {
function ref(id) {
const { doc, getFirestore } = firestoreModular;
return doc(getFirestore(), `${COLLECTION}/vector_${id}`);
}

it('writes and reads a vector', async function () {
const { setDoc, getDoc, vector } = firestoreModular;

const r = ref('basic');
await setDoc(r, { embedding: vector([0.12, 0.34, 0.56]) });

const snap = await getDoc(r);
const v = snap.get('embedding');
should.exist(v);
v.toArray().should.eql([0.12, 0.34, 0.56]);
});

it('supports vectors in nested structures', async function () {
const { setDoc, getDoc, vector } = firestoreModular;

const r = ref('nested');
await setDoc(r, {
a: { b: vector([1, 2, 3]) },
});

const snap = await getDoc(r);
snap.get('a').b.toArray().should.eql([1, 2, 3]);
});

it('updates a vector field', async function () {
const { setDoc, getDoc, updateDoc, vector } = firestoreModular;

const r = ref('update');
await setDoc(r, { x: 1 });
await updateDoc(r, { embedding: vector([9, 8, 7]) });

const snap = await getDoc(r);
snap.get('embedding').toArray().should.eql([9, 8, 7]);
});

it('batch writes a vector', async function () {
const { getFirestore, writeBatch, getDoc, vector } = firestoreModular;
const r = ref('batch');
const b = writeBatch(getFirestore());
b.set(r, { embedding: vector([0.1, 0.2]) });
await b.commit();

const snap = await getDoc(r);
snap.get('embedding').toArray().should.eql([0.1, 0.2]);
});

it('transaction writes a vector', async function () {
const { getFirestore, runTransaction, getDoc, vector } = firestoreModular;
const r = ref('transaction');
await runTransaction(getFirestore(), async tx => {
tx.set(r, { embedding: vector([3.14, 2.72]) });
});

const snap = await getDoc(r);
snap.get('embedding').toArray().should.eql([3.14, 2.72]);
});
});
});
15 changes: 14 additions & 1 deletion packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
*
*/

#import "RNFBFirestoreSerialize.h"
#import "FirebaseFirestore/FIRVectorValue.h"

#import "RNFBFirestoreCommon.h"
#import "RNFBFirestoreSerialize.h"
#import "RNFBPreferences.h"

@implementation RNFBFirestoreSerialize
Expand Down Expand Up @@ -58,6 +60,7 @@ @implementation RNFBFirestoreSerialize
INT_OBJECT,
INT_INTEGER,
INT_NEGATIVE_ZERO,
INT_VECTOR,
INT_UNKNOWN = -999,
};

Expand Down Expand Up @@ -358,6 +361,14 @@ + (NSArray *)buildTypeMap:(id)value {
return typeArray;
}

// Vector
if ([value isKindOfClass:[FIRVectorValue class]]) {
FIRVectorValue *vector = (FIRVectorValue *)value;
typeArray[0] = @(INT_VECTOR);
typeArray[1] = vector.array;
return typeArray;
}

typeArray[0] = @(INT_UNKNOWN);
return typeArray;
}
Expand Down Expand Up @@ -466,6 +477,8 @@ + (id)parseTypeMap:(FIRFirestore *)firestore typeMap:(NSArray *)typeMap {
}
case INT_OBJECT:
return [self parseNSDictionary:firestore dictionary:typeMap[1]];
case INT_VECTOR:
return [FIRFieldValue vectorWithArray:typeMap[1]];
case INT_UNKNOWN:
default:
return nil;
Expand Down
5 changes: 5 additions & 0 deletions packages/firestore/lib/FirestoreStatics.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@ import FirestoreFieldValue from './FirestoreFieldValue';
import FirestoreGeoPoint from './FirestoreGeoPoint';
import FirestoreTimestamp from './FirestoreTimestamp';
import { Filter } from './FirestoreFilter';
import FirestoreVectorValue from './FirestoreVectorValue';
export default {
Blob: FirestoreBlob,
FieldPath: FirestoreFieldPath,
FieldValue: createDeprecationProxy(FirestoreFieldValue),
GeoPoint: FirestoreGeoPoint,
Timestamp: createDeprecationProxy(FirestoreTimestamp),
Filter: createDeprecationProxy(Filter),
VectorValue: FirestoreVectorValue,
vector(values) {
return new FirestoreVectorValue(values);
},

CACHE_SIZE_UNLIMITED: -1,

Expand Down
75 changes: 75 additions & 0 deletions packages/firestore/lib/FirestoreVectorValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library 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 { isArray, isNumber } from '@react-native-firebase/app/lib/common';

export default class FirestoreVectorValue {
constructor(values) {
if (values === undefined) {
this._values = [];
return;
}

if (!isArray(values)) {
throw new Error(
"firebase.firestore.VectorValue(values?) 'values' expected an array of numbers or undefined.",
);
}

for (let i = 0; i < values.length; i++) {
const v = values[i];
if (!isNumber(v)) {
throw new Error(
`firebase.firestore.VectorValue(values?) 'values[${i}]' expected a number value.`,
);
}
}

// Store a shallow copy to ensure immutability semantics for the input array
this._values = values.slice();
}

static fromJSON(json) {
parsedVector = JSON.parse(json);
return new FirestoreVectorValue(parsedVector.vectorValues);
}

isEqual(other) {
if (!(other instanceof FirestoreVectorValue)) {
throw new Error(
"firebase.firestore.VectorValue.isEqual(*) 'other' expected a VectorValue instance.",
);
}

const a = this._values;
const b = other._values;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
// Use strict equality; Firestore numbers allow NaN/Infinity – equality semantics match JS
if (a[i] !== b[i]) return false;
}
return true;
}

toArray() {
return this._values.slice();
}

toJSON() {
return { vectorValues: this._values.slice() };
}
}
30 changes: 30 additions & 0 deletions packages/firestore/lib/modular/VectorValue.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Represents a vector type in Firestore documents. Create an instance with vector().
*/
export declare class VectorValue {
// Note the values array and constructor are not public APIs.

/**
* Builds a VectorValue instance from a JSON object created by VectorValue.toJSON().
*
* @param json a JSON object represention of a VectorValue instance.
*/
static fromJSON(json: object): VectorValue;

/**
* Returns true if the two VectorValue values have the same raw number arrays, returns false otherwise.
*/
isEqual(other: VectorValue): boolean;

/**
* Returns a copy of the raw number array form of the vector.
*/
toArray(): number[];

/**
* Returns a JSON-serializable representation of this VectorValue instance.
*/
toJSON(): { values: number[] };
}

export declare function vector(values?: number[]): VectorValue;
11 changes: 11 additions & 0 deletions packages/firestore/lib/modular/VectorValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import FirestoreVectorValue from '../FirestoreVectorValue';

export const VectorValue = FirestoreVectorValue;

/**
* @param {number[]=} values
* @returns {VectorValue}
*/
export function vector(values) {
return new VectorValue(values);
}
1 change: 1 addition & 0 deletions packages/firestore/lib/modular/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,3 +785,4 @@ export * from './FieldPath';
export * from './FieldValue';
export * from './GeoPoint';
export * from './Timestamp';
export * from './VectorValue';
1 change: 1 addition & 0 deletions packages/firestore/lib/modular/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,4 +407,5 @@ export * from './FieldPath';
export * from './FieldValue';
export * from './GeoPoint';
export * from './Timestamp';
export * from './VectorValue';
export { Filter } from '../FirestoreFilter';
Loading
Loading