Skip to content

Commit 910d4e4

Browse files
authored
fix(firestore)!: fix Long/Double conversion issues #3004 (#5840)
* feat(firestore): fix iOS Long/Double conversion #3004 * Add special -0 handling * Add tests Co-Authored-By: Mike Hardy <[email protected]> BREAKING CHANGE: Previous versions of firestore here incorrectly saved integers as doubles on iOS, so they did not show up in `where`/`in` queries. You had to save numbers as strings if you wanted `where`/`in` queries to work cross-platform. Number types will now be handled correctly. However, If you have integers saved (incorrectly!) as double (from previous versions) and you use where / in style queries on numbers, then the same document will no longer be found via .where. Mitigation could be to go through your whole DB and load and re-save the integers correctly, or alter queries. Please test your where / in queries that use number types if this affects you.
1 parent 3ebe5bb commit 910d4e4

File tree

5 files changed

+99
-22
lines changed

5 files changed

+99
-22
lines changed

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreSerialize.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public class ReactNativeFirebaseFirestoreSerialize {
6060
private static final int INT_DOCUMENTID = 4;
6161
private static final int INT_BOOLEAN_TRUE = 5;
6262
private static final int INT_BOOLEAN_FALSE = 6;
63-
private static final int INT_NUMBER = 7;
63+
private static final int INT_DOUBLE = 7;
6464
private static final int INT_STRING = 8;
6565
private static final int INT_STRING_EMPTY = 9;
6666
private static final int INT_ARRAY = 10;
@@ -70,6 +70,8 @@ public class ReactNativeFirebaseFirestoreSerialize {
7070
private static final int INT_BLOB = 14;
7171
private static final int INT_FIELDVALUE = 15;
7272
private static final int INT_OBJECT = 16;
73+
private static final int INT_INTEGER = 17;
74+
private static final int INT_NEGATIVE_ZERO = 18;
7375
private static final int INT_UNKNOWN = -999;
7476

7577
// Keys
@@ -300,7 +302,7 @@ private static WritableArray buildTypeMap(Object value) {
300302
}
301303

302304
if (value instanceof Integer) {
303-
typeArray.pushInt(INT_NUMBER);
305+
typeArray.pushInt(INT_DOUBLE);
304306
typeArray.pushDouble(((Integer) value).doubleValue());
305307
return typeArray;
306308
}
@@ -325,19 +327,19 @@ private static WritableArray buildTypeMap(Object value) {
325327
return typeArray;
326328
}
327329

328-
typeArray.pushInt(INT_NUMBER);
330+
typeArray.pushInt(INT_DOUBLE);
329331
typeArray.pushDouble(doubleValue);
330332
return typeArray;
331333
}
332334

333335
if (value instanceof Float) {
334-
typeArray.pushInt(INT_NUMBER);
336+
typeArray.pushInt(INT_DOUBLE);
335337
typeArray.pushDouble(((Float) value).doubleValue());
336338
return typeArray;
337339
}
338340

339341
if (value instanceof Long) {
340-
typeArray.pushInt(INT_NUMBER);
342+
typeArray.pushInt(INT_DOUBLE);
341343
typeArray.pushDouble(((Long) value).doubleValue());
342344
return typeArray;
343345
}
@@ -461,14 +463,12 @@ static Object parseTypeMap(FirebaseFirestore firestore, ReadableArray typeArray)
461463
return true;
462464
case INT_BOOLEAN_FALSE:
463465
return false;
464-
case INT_NUMBER:
465-
// https://github.com/invertase/react-native-firebase/issues/3004
466-
// Number values come from JS as Strings on Android so we can check for floating points
467-
String numberStringValue = Objects.requireNonNull(typeArray.getString(1));
468-
if (numberStringValue.contains(".")) {
469-
return Double.valueOf(numberStringValue);
470-
}
471-
return Long.valueOf(numberStringValue, 10);
466+
case INT_NEGATIVE_ZERO:
467+
return -0.0;
468+
case INT_INTEGER:
469+
return (long) typeArray.getDouble(1);
470+
case INT_DOUBLE:
471+
return typeArray.getDouble(1);
472472
case INT_STRING:
473473
return typeArray.getString(1);
474474
case INT_STRING_EMPTY:

packages/firestore/e2e/issues.e2e.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@
1616
*/
1717

1818
const COLLECTION = 'firestore';
19+
const { getE2eEmulatorHost } = require('@react-native-firebase/app/e2e/helpers');
20+
const jsFirebase = require('firebase/compat/app');
21+
require('firebase/compat/firestore');
22+
23+
const testNumbers = {
24+
zero: 0, // int
25+
negativeZero: -0, // double
26+
half: 0.5, // double
27+
unsafeInt: Number.MAX_SAFE_INTEGER + 1, // double
28+
nagativeUnsafeInt: Number.MIN_SAFE_INTEGER - 1, // double
29+
safeInt: Number.MAX_SAFE_INTEGER, // int
30+
nagativeSafeInt: Number.MIN_SAFE_INTEGER, // int
31+
inf: Infinity, // double
32+
negativeInf: -Infinity, // double
33+
// nan: NaN, // double -- where-in queries on NaN do not work
34+
};
1935

2036
describe('firestore()', function () {
2137
describe('issues', function () {
@@ -92,4 +108,51 @@ describe('firestore()', function () {
92108
});
93109
});
94110
});
111+
112+
describe('number type consistency', function () {
113+
before(async function () {
114+
jsFirebase.initializeApp(FirebaseHelpers.app.config());
115+
jsFirebase.firestore().useEmulator(getE2eEmulatorHost(), 8080);
116+
117+
// Put one example of each number in our collection using JS SDK
118+
await Promise.all(
119+
Object.entries(testNumbers).map(([testName, testValue]) => {
120+
return jsFirebase
121+
.firestore()
122+
.doc(`${COLLECTION}/numberTestsJS/cases/${testName}`)
123+
.set({ number: testValue });
124+
}),
125+
);
126+
127+
// Put one example of each number in our collection using Native SDK
128+
await Promise.all(
129+
Object.entries(testNumbers).map(([testName, testValue]) => {
130+
return firebase
131+
.firestore()
132+
.doc(`${COLLECTION}/numberTestsNative/cases/${testName}`)
133+
.set({ number: testValue });
134+
}),
135+
);
136+
});
137+
138+
it('types inserted by JS may be queried by native with filters', async function () {
139+
const testValues = Object.values(testNumbers);
140+
const ref = firebase
141+
.firestore()
142+
.collection(`${COLLECTION}/numberTestsJS/cases`)
143+
.where('number', 'in', testValues);
144+
typesSnap = await ref.get();
145+
should.deepEqual(typesSnap.docs.map(d => d.id).sort(), Object.keys(testNumbers).sort());
146+
});
147+
148+
it('types inserted by native may be queried by JS with filters', async function () {
149+
const testValues = Object.values(testNumbers);
150+
const ref = jsFirebase
151+
.firestore()
152+
.collection(`${COLLECTION}/numberTestsNative/cases`)
153+
.where('number', 'in', testValues);
154+
typesSnap = await ref.get();
155+
should.deepEqual(typesSnap.docs.map(d => d.id).sort(), Object.keys(testNumbers).sort());
156+
});
157+
});
95158
});

packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ @implementation RNFBFirestoreSerialize
4646
INT_DOCUMENTID,
4747
INT_BOOLEAN_TRUE,
4848
INT_BOOLEAN_FALSE,
49-
INT_NUMBER,
49+
INT_DOUBLE,
5050
INT_STRING,
5151
INT_STRING_EMPTY,
5252
INT_ARRAY,
@@ -56,6 +56,8 @@ @implementation RNFBFirestoreSerialize
5656
INT_BLOB,
5757
INT_FIELDVALUE,
5858
INT_OBJECT,
59+
INT_INTEGER,
60+
INT_NEGATIVE_ZERO,
5961
INT_UNKNOWN = -999,
6062
};
6163

@@ -336,7 +338,7 @@ + (NSArray *)buildTypeMap:(id)value {
336338
}
337339

338340
// Number
339-
typeArray[0] = @(INT_NUMBER);
341+
typeArray[0] = @(INT_DOUBLE);
340342
typeArray[1] = value;
341343
return typeArray;
342344
}
@@ -400,7 +402,11 @@ + (id)parseTypeMap:(FIRFirestore *)firestore typeMap:(NSArray *)typeMap {
400402
return @(YES);
401403
case INT_BOOLEAN_FALSE:
402404
return @(NO);
403-
case INT_NUMBER:
405+
case INT_NEGATIVE_ZERO:
406+
return @(-0.0);
407+
case INT_INTEGER:
408+
return @([typeMap[1] longLongValue]);
409+
case INT_DOUBLE:
404410
return @([typeMap[1] doubleValue]);
405411
case INT_STRING:
406412
return typeMap[1];

packages/firestore/lib/utils/serialize.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
*/
1717

1818
import {
19-
isAndroid,
2019
isArray,
2120
isBoolean,
2221
isDate,
@@ -139,10 +138,15 @@ export function generateNativeData(value, ignoreUndefined) {
139138
}
140139

141140
if (isNumber(value)) {
142-
if (isAndroid) {
143-
return getTypeMapInt('number', value.toString());
141+
// mirror the JS SDK's integer detection algorithm
142+
// https://github.com/firebase/firebase-js-sdk/blob/086df7c7e0299cedd9f3cff9080f46ca25cab7cd/packages/firestore/src/remote/number_serializer.ts#L56
143+
if (value === 0 && 1 / value === -Infinity) {
144+
return getTypeMapInt('negativeZero');
144145
}
145-
return getTypeMapInt('number', value);
146+
if (Number.isSafeInteger(value)) {
147+
return getTypeMapInt('integer', value);
148+
}
149+
return getTypeMapInt('double', value);
146150
}
147151

148152
if (isString(value)) {
@@ -254,7 +258,9 @@ export function parseNativeData(firestore, nativeArray) {
254258
return true;
255259
case 'booleanFalse':
256260
return false;
257-
case 'number':
261+
case 'double':
262+
case 'integer':
263+
case 'negativeZero':
258264
case 'string':
259265
return value;
260266
case 'stringEmpty':

packages/firestore/lib/utils/typemap.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const MAP = {
2525
documentid: 4, // to native only
2626
booleanTrue: 5,
2727
booleanFalse: 6,
28-
number: 7,
28+
double: 7,
2929
string: 8,
3030
stringEmpty: 9,
3131
array: 10,
@@ -35,6 +35,8 @@ const MAP = {
3535
blob: 14,
3636
fieldvalue: 15,
3737
object: 16,
38+
integer: 17,
39+
negativeZero: 18,
3840
unknown: -999,
3941
};
4042

0 commit comments

Comments
 (0)