Skip to content
Open
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
37 changes: 37 additions & 0 deletions packages/firestore/__tests__/firestore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1431,4 +1431,41 @@ describe('Firestore', function () {
});
});
});

describe('VectorValue (unit serializer)', function () {
it('constructs and validates values', function () {
const { default: FirestoreVectorValue } = require('../lib/FirestoreVectorValue');
const v = new FirestoreVectorValue([0, 1.5, -2]);
expect(v.values).toEqual([0, 1.5, -2]);
expect(v.isEqual(new FirestoreVectorValue([0, 1.5, -2]))).toBe(true);
expect(v.isEqual(new FirestoreVectorValue([0, 1.5]))).toBe(false);
});

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

const v = new FirestoreVectorValue([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 instanceof FirestoreVectorValue).toBe(true);
expect(parsed.values).toEqual([0.1, 0.2, 0.3]);
});

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

const v = new FirestoreVectorValue([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 @@ -71,6 +71,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 +405,36 @@ private static WritableArray buildTypeMap(Object value) {
return typeArray;
}

// VectorValue – detect via reflection to avoid compile-time dependency on newer SDKs
try {
Class<?> vectorClass = Class.forName("com.google.firebase.firestore.VectorValue");
if (vectorClass.isInstance(value)) {
typeArray.pushInt(INT_VECTOR);
WritableArray valuesArray = Arguments.createArray();
try {
double[] doubles = (double[]) vectorClass.getMethod("getValues").invoke(value);
if (doubles != null) {
for (double d : doubles) valuesArray.pushDouble(d);
}
} catch (Exception ignored) {
try {
Object result = vectorClass.getMethod("values").invoke(value);
if (result instanceof double[]) {
for (double d : (double[]) result) valuesArray.pushDouble(d);
} else if (result instanceof List) {
for (Object o : (List<?>) result) valuesArray.pushDouble(((Number) o).doubleValue());
}
} catch (Exception ignored2) {
// leave empty if not accessible
}
}
typeArray.pushArray(valuesArray);
return typeArray;
}
} catch (ClassNotFoundException e) {
// Older SDK without VectorValue – fall through
}

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

typeArray.pushInt(INT_UNKNOWN);
Expand Down Expand Up @@ -520,6 +551,26 @@ static Object parseTypeMap(FirebaseFirestore firestore, ReadableArray typeArray)
}
case INT_OBJECT:
return parseReadableMap(firestore, typeArray.getMap(1));
case INT_VECTOR:
try {
Class<?> vectorClass = Class.forName("com.google.firebase.firestore.VectorValue");
ReadableArray vals = typeArray.getArray(1);
if (vals == null) return null;
double[] doubles = new double[vals.size()];
for (int i = 0; i < vals.size(); i++) doubles[i] = vals.getDouble(i);
try {
// Prefer static factory if available
return vectorClass.getMethod("from", double[].class).invoke(null, (Object) doubles);
} catch (Exception noFactory) {
try {
return vectorClass.getConstructor(double[].class).newInstance((Object) doubles);
} catch (Exception noCtor) {
return null;
}
}
} catch (ClassNotFoundException e) {
return null;
}
case INT_UNKNOWN:
default:
return null;
Expand Down
103 changes: 103 additions & 0 deletions packages/firestore/e2e/VectorValue.e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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.
*
*/

describe('firestore.VectorValue', function () {
describe('v8 compatibility', function () {
beforeEach(async function beforeEachTest() {
// @ts-ignore
globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true;
});

afterEach(async function afterEachTest() {
// @ts-ignore
globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false;
});

function ref(id) {
return firebase.firestore().doc(`e2e_vector/${id}`);
}

it('writes and reads a vector', async function () {
const r = ref('basic');
await r.set({ embedding: firebase.firestore.vector([0.12, 0.34, 0.56]) });

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

it('supports vectors in nested structures', async function () {
const r = ref('nested');
await r.set({
a: { b: firebase.firestore.vector([1, 2, 3]) },
arr: [firebase.firestore.vector([4, 5])],
});

const snap = await r.get();
snap.get('a').b.values.should.eql([1, 2, 3]);
snap.get('arr')[0].values.should.eql([4, 5]);
});

it('updates a vector field', async function () {
const r = ref('update');
await r.set({ x: 1 });
await r.update({ embedding: firebase.firestore.vector([9, 8, 7]) });

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

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

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

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

const snap = await r.get();
snap.get('embedding').values.should.eql([3.14, 2.72]);
});
});

describe('modular', function () {
function ref(id) {
// @ts-ignore test env provides firestoreModular
return firestoreModular.doc(firestoreModular.getFirestore(), `e2e_vector/${id}`);
}

it('writes and reads using modular vector()', async function () {
// @ts-ignore test env provides firestoreModular
const { setDoc, getDoc, vector } = firestoreModular;
const r = ref('modular-basic');
await setDoc(r, { embedding: vector([0.5, 0.25]) });
const snap = await getDoc(r);
const v = snap.get('embedding');
should.exist(v);
v.values.should.eql([0.5, 0.25]);
});
});
});
36 changes: 36 additions & 0 deletions packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ @implementation RNFBFirestoreSerialize
INT_OBJECT,
INT_INTEGER,
INT_NEGATIVE_ZERO,
INT_VECTOR,
INT_UNKNOWN = -999,
};

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

// VectorValue (FIRVectorValue) – detect reflectively to avoid hard dependency on symbol
Class vectorClass = NSClassFromString(@"FIRVectorValue");
if (vectorClass != nil && [value isKindOfClass:vectorClass]) {
typeArray[0] = @(INT_VECTOR);
NSArray *values = nil;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([value respondsToSelector:@selector(values)]) {
values = [value performSelector:@selector(values)];
}
#pragma clang diagnostic pop
if (values == nil) {
values = @[];
}
typeArray[1] = values;
return typeArray;
}

typeArray[0] = @(INT_UNKNOWN);
return typeArray;
}
Expand Down Expand Up @@ -466,6 +485,23 @@ + (id)parseTypeMap:(FIRFirestore *)firestore typeMap:(NSArray *)typeMap {
}
case INT_OBJECT:
return [self parseNSDictionary:firestore dictionary:typeMap[1]];
case INT_VECTOR: {
NSArray *values = typeMap[1];
Class vectorClass = NSClassFromString(@"FIRVectorValue");
if (vectorClass != nil) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([vectorClass respondsToSelector:@selector(vectorWithValues:)]) {
return [vectorClass performSelector:@selector(vectorWithValues:) withObject:values];
}
id instance = [vectorClass alloc];
if ([instance respondsToSelector:@selector(initWithValues:)]) {
return [instance performSelector:@selector(initWithValues:) withObject:values];
}
#pragma clang diagnostic pop
}
return nil;
}
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
74 changes: 74 additions & 0 deletions packages/firestore/lib/FirestoreVectorValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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();
}

get values() {
return this._values.slice();
}

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;
}

toJSON() {
return { values: this._values.slice() };
}

toString() {
return `FirestoreVectorValue(values=[${this._values.join(', ')}])`;
}
}
Loading