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
107 changes: 103 additions & 4 deletions packages/firestore/src/model/transform_operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ import { arrayEquals } from '../util/misc';

import { normalizeNumber } from './normalize';
import { serverTimestamp } from './server_timestamps';
import { isArray, isIntegerValue, isNumber, valueEquals } from './values';
import {
isArray,
isIntegerValue,
isNumber,
valueEquals,
isDecimal128Value,
isInt32Value,
isDoubleValue,
RESERVED_DECIMAL128_KEY,
RESERVED_INT32_KEY
} from './values';

/** Used to represent a field transform on a mutation. */
export class TransformOperation {
Expand Down Expand Up @@ -229,7 +239,42 @@ export function applyNumericIncrementTransformOperationToLocalView(
previousValue
)!;
const sum = asNumber(baseValue) + asNumber(transform.operand);
if (isIntegerValue(baseValue) && isIntegerValue(transform.operand)) {
if (isDecimal128Value(baseValue) || isDecimal128Value(transform.operand)) {
return {
mapValue: {
fields: {
[RESERVED_DECIMAL128_KEY]: {
stringValue: sum.toString()
}
}
}
};
}
if (isInt32Value(baseValue)) {
if (isIntegerValue(transform.operand) || isInt32Value(transform.operand)) {
return {
mapValue: {
fields: {
[RESERVED_INT32_KEY]: toInteger(sum)
}
}
};
} else {
return {
mapValue: {
fields: {
[RESERVED_DECIMAL128_KEY]: {
stringValue: sum.toString()
}
}
}
};
}
}
if (
isIntegerValue(baseValue) &&
(isIntegerValue(transform.operand) || isInt32Value(transform.operand))
) {
return toInteger(sum);
Comment thread
dlarocque marked this conversation as resolved.
} else {
return toDouble(transform.serializer, sum);
Expand All @@ -247,7 +292,45 @@ export function applyNumericTransformOperationToLocalView(
const prev = asNumber(previousValue);
const oper = asNumber(operation.operand);
const result = transform(prev, oper);
if (isIntegerValue(previousValue) && isIntegerValue(operation.operand)) {
if (
isDecimal128Value(previousValue) ||
isDecimal128Value(operation.operand)
) {
return {
mapValue: {
fields: {
[RESERVED_DECIMAL128_KEY]: {
stringValue: result.toString()
}
}
}
};
}
if (isInt32Value(previousValue)) {
if (isIntegerValue(operation.operand) || isInt32Value(operation.operand)) {
return {
mapValue: {
fields: {
[RESERVED_INT32_KEY]: toInteger(result)
}
}
};
} else {
return {
mapValue: {
fields: {
[RESERVED_DECIMAL128_KEY]: {
stringValue: result.toString()
}
}
}
};
}
}
if (
isIntegerValue(previousValue) &&
(isIntegerValue(operation.operand) || isInt32Value(operation.operand))
) {
return toInteger(result);
Comment thread
dlarocque marked this conversation as resolved.
} else {
return toDouble(operation.serializer, result);
Expand Down Expand Up @@ -277,7 +360,23 @@ export function applyNumericMaximumTransformOperationToLocalView(
}

function asNumber(value: ProtoValue): number {
return normalizeNumber(value.integerValue || value.doubleValue);
if (isIntegerValue(value)) {
return normalizeNumber(value.integerValue);
}
if (isDoubleValue(value)) {
return normalizeNumber(value.doubleValue);
}
if (isInt32Value(value)) {
return normalizeNumber(
value.mapValue!.fields![RESERVED_INT32_KEY]!.integerValue!
);
}
if (isDecimal128Value(value)) {
return parseFloat(
value.mapValue!.fields![RESERVED_DECIMAL128_KEY]!.stringValue!
);
}
Comment thread
dlarocque marked this conversation as resolved.
return 0;
}

function coercedFieldValuesArray(value: ProtoValue | null): ProtoValue[] {
Expand Down
10 changes: 8 additions & 2 deletions packages/firestore/src/model/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -964,8 +964,14 @@ export function isNumber(
value?: Value | null
): value is
| { doubleValue: string | number }
| { integerValue: string | number } {
return isIntegerValue(value) || isDoubleValue(value);
| { integerValue: string | number }
| { mapValue: MapValue } {
return (
isIntegerValue(value) ||
isDoubleValue(value) ||
(!!value && isInt32Value(value)) ||
(!!value && isDecimal128Value(value))
);
}

/** Returns true if `value` is an ArrayValue. */
Expand Down
152 changes: 150 additions & 2 deletions packages/firestore/test/unit/model/mutation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import {
increment,
Timestamp,
serverTimestamp,
deleteField
deleteField,
Int32Value,
Decimal128Value
} from '../../../src';
import { MutableDocument } from '../../../src/model/document';
import { FieldMask } from '../../../src/model/field_mask';
Expand All @@ -39,8 +41,11 @@ import {
import { serverTimestamp as serverTimestampInternal } from '../../../src/model/server_timestamps';
import {
ArrayRemoveTransformOperation,
ArrayUnionTransformOperation
ArrayUnionTransformOperation,
NumericIncrementTransformOperation,
applyNumericIncrementTransformOperationToLocalView
} from '../../../src/model/transform_operation';
import { Serializer } from '../../../src/remote/number_serializer';
import { Dict } from '../../../src/util/obj';
import { addEqualityMatcher } from '../../util/equality_matcher';
import {
Expand All @@ -66,6 +71,7 @@ describe('Mutation', () => {
addEqualityMatcher();

const timestamp = Timestamp.now();
const dummySerializer = { useProto3Json: false } as unknown as Serializer;

/**
* For each document in `docs`, calculate the overlay mutations of each
Expand Down Expand Up @@ -564,6 +570,148 @@ describe('Mutation', () => {
verifyTransform(baseDoc, transform, expected);
});

it('correctly performs numeric increment on BSON Int32Value local evaluation', () => {
const baseDoc = { int32Val: new Int32Value(10) };
const transform = { int32Val: increment(5) };
const expected = { int32Val: new Int32Value(15) };
verifyTransform(baseDoc, transform, expected);
});

// Although the API restricts increment operands to standard JS numbers (not BSON numbers),
// the underlying model class represents a generic Firestore transform where the operand can
// be any valid ProtoValue (including BSON Int32Value/Decimal128Value). We test these cases to
// ensure the model layer's local evaluation is robust.

// Int32Value + Int32Value -> Int32Value
it('correctly performs mixed-type numeric increment of Int32Value base by Int32Value operand', () => {
const op = new NumericIncrementTransformOperation(
dummySerializer,
wrap(new Int32Value(5))
);
const res = applyNumericIncrementTransformOperationToLocalView(
op,
wrap(new Int32Value(10))
);
expect(res).to.deep.equal(wrap(new Int32Value(15)));
});

// Standard Int + Int32Value -> Standard Int
it('correctly performs mixed-type numeric increment of standard integer base by Int32Value operand', () => {
const op = new NumericIncrementTransformOperation(
dummySerializer,
wrap(new Int32Value(5))
);
const res = applyNumericIncrementTransformOperationToLocalView(
op,
wrap(10)
);
expect(res).to.deep.equal(wrap(15));
});

// Decimal128Value + Standard Int -> Decimal128Value
it('correctly performs mixed-type numeric increment promoting Decimal128Value base by standard integer operand', () => {
const op = new NumericIncrementTransformOperation(dummySerializer, wrap(5));
const res = applyNumericIncrementTransformOperationToLocalView(
op,
wrap(new Decimal128Value('10'))
);
expect(res).to.deep.equal(wrap(new Decimal128Value('15')));
});

// Standard Int + Decimal128Value -> Decimal128Value
it('correctly performs mixed-type numeric increment promoting standard integer base by Decimal128Value operand', () => {
const op = new NumericIncrementTransformOperation(
dummySerializer,
wrap(new Decimal128Value('5'))
);
const res = applyNumericIncrementTransformOperationToLocalView(
op,
wrap(10)
);
expect(res).to.deep.equal(wrap(new Decimal128Value('15')));
});

// Int32Value + Decimal128Value -> Decimal128Value
it('correctly performs mixed-type numeric increment promoting Int32Value base by Decimal128Value operand', () => {
const op = new NumericIncrementTransformOperation(
dummySerializer,
wrap(new Decimal128Value('5'))
);
const res = applyNumericIncrementTransformOperationToLocalView(
op,
wrap(new Int32Value(10))
);
expect(res).to.deep.equal(wrap(new Decimal128Value('15')));
});

// Int32Value + Standard Double -> Decimal128Value (promotion)
it('correctly performs mixed-type numeric increment promoting Int32Value base by standard double operand', () => {
const op = new NumericIncrementTransformOperation(
dummySerializer,
wrap(1.5)
);
const res = applyNumericIncrementTransformOperationToLocalView(
op,
wrap(new Int32Value(10))
);
expect(res).to.deep.equal(wrap(new Decimal128Value('11.5')));
});

// Standard Double + Int32Value -> Standard Double
it('correctly performs mixed-type numeric increment of standard double base by Int32Value operand', () => {
const op = new NumericIncrementTransformOperation(
dummySerializer,
wrap(new Int32Value(5))
);
const res = applyNumericIncrementTransformOperationToLocalView(
op,
wrap(10.5)
);
expect(res).to.deep.equal(wrap(15.5));
});

// Decimal128Value + Decimal128Value -> Decimal128Value
it('correctly performs mixed-type numeric increment of Decimal128Value base by Decimal128Value operand', () => {
const op = new NumericIncrementTransformOperation(
dummySerializer,
wrap(new Decimal128Value('5'))
);
const res = applyNumericIncrementTransformOperationToLocalView(
op,
wrap(new Decimal128Value('10'))
);
expect(res).to.deep.equal(wrap(new Decimal128Value('15')));
});

// Standard Double + Decimal128Value -> Decimal128Value
it('correctly performs mixed-type numeric increment promoting standard double base by Decimal128Value operand', () => {
const op = new NumericIncrementTransformOperation(
dummySerializer,
wrap(new Decimal128Value('5'))
);
const res = applyNumericIncrementTransformOperationToLocalView(
op,
wrap(10.5)
);
expect(res).to.deep.equal(wrap(new Decimal128Value('15.5')));
});

// verifyTransform: Int32Value + Standard Double -> Decimal128Value (promotion)
it('correctly promotes Int32Value base to Decimal128Value when incremented by a double in verifyTransform', () => {
const baseDoc = { value: new Int32Value(10) };
const transform = { value: increment(1.5) };
const expected = { value: new Decimal128Value('11.5') };
verifyTransform(baseDoc, transform, expected);
});

// verifyTransform: Decimal128Value + Standard Int -> Decimal128Value
it('retains Decimal128Value type when incrementing by an integer in verifyTransform', () => {
const baseDoc = { value: new Decimal128Value('10') };
const transform = { value: increment(5) };
const expected = { value: new Decimal128Value('15') };
verifyTransform(baseDoc, transform, expected);
});

it('can apply numeric add transform to missing field', () => {
const baseDoc = {};
const transform = { missing: increment(1) };
Expand Down
Loading