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
8 changes: 0 additions & 8 deletions .github/scripts/compare-types/configs/firestore-pipelines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@ import type { PackageConfig } from '../src/types';
const config: PackageConfig = {
nameMapping: {},
missingInRN: [
{
name: 'arrayFirst',
reason: 'Newer firebase-js-sdk array expression helper not yet exposed by RN Firebase pipelines.',
},
{
name: 'arrayFirstN',
reason: 'Newer firebase-js-sdk array expression helper not yet exposed by RN Firebase pipelines.',
},
{
name: 'arrayIndexOf',
reason: 'Newer firebase-js-sdk array expression helper not yet exposed by RN Firebase pipelines.',
Expand Down
18 changes: 18 additions & 0 deletions packages/firestore/__tests__/pipelines-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ const ANDROID_EXECUTOR_PATH = join(
ROOT,
'packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestorePipelineParser.java',
);
const ANDROID_NODE_BUILDER_PATH = join(
ROOT,
'packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestorePipelineNodeBuilder.java',
);
const IOS_EXECUTOR_PATH = join(
ROOT,
'packages/firestore/ios/RNFBFirestore/RNFBFirestorePipelineParser.swift',
);
const IOS_NODE_BUILDER_PATH = join(
ROOT,
'packages/firestore/ios/RNFBFirestore/RNFBFirestorePipelineNodeBuilder.swift',
);

function extractQuotedList(source: string, marker: string, endMarker: string): string[] {
const markerIndex = source.indexOf(marker);
Expand Down Expand Up @@ -79,4 +87,14 @@ describe('Firestore pipeline native parity', function () {
expect(iosSource).toContain('does not support options.rawOptions on iOS');
expect(iosSource).toContain('does not support pipeline.source.rawOptions');
});

it('keeps arrayFirst and arrayFirstN on native lowering paths', function () {
const androidSource = readFileSync(ANDROID_NODE_BUILDER_PATH, 'utf8');
const iosSource = readFileSync(IOS_NODE_BUILDER_PATH, 'utf8');

expect(androidSource).toContain('currentExpression.arrayFirst()');
expect(androidSource).toContain('arrayExpr.arrayFirstN');
expect(iosSource).toContain('"array_first"');
expect(iosSource).toContain('"array_first_n"');
});
});
80 changes: 80 additions & 0 deletions packages/firestore/__tests__/pipelines.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, expect, it, jest } from '@jest/globals';
import { firebase } from '../lib';
import {
arrayFilter,
arrayFirst,
arrayFirstN,
arrayGet,
and,
conditional,
Expand Down Expand Up @@ -205,6 +207,78 @@ describe('Firestore pipelines runtime', function () {
});
});

it('serializes arrayFirst and arrayFirstN as function expression helpers', function () {
const db: any = firebase.firestore();
const serialized = db
.pipeline()
.collection('firestore')
.select(
arrayFirst('items').as('firstItem'),
arrayFirstN(field('items'), 2).as('firstTwoItems'),
arrayFirstN('items', field('count')).as('dynamicFirstItems'),
field('items').arrayFirst().as('fluentFirstItem'),
field('items').arrayFirstN(2).as('fluentFirstTwoItems'),
)
.serialize();

expect(serialized.stages[0]).toMatchObject({
stage: 'select',
options: {
selections: [
{
alias: 'firstItem',
expr: {
exprType: 'Function',
name: 'arrayFirst',
args: [{ exprType: 'Field', path: 'items' }],
},
},
{
alias: 'firstTwoItems',
expr: {
exprType: 'Function',
name: 'arrayFirstN',
args: [
{ exprType: 'Field', path: 'items' },
{ exprType: 'Constant', value: 2 },
],
},
},
{
alias: 'dynamicFirstItems',
expr: {
exprType: 'Function',
name: 'arrayFirstN',
args: [
{ exprType: 'Field', path: 'items' },
{ exprType: 'Field', path: 'count' },
],
},
},
{
alias: 'fluentFirstItem',
expr: {
exprType: 'Function',
name: 'arrayFirst',
args: [{ exprType: 'Field', path: 'items' }],
},
},
{
alias: 'fluentFirstTwoItems',
expr: {
exprType: 'Function',
name: 'arrayFirstN',
args: [
{ exprType: 'Field', path: 'items' },
{ exprType: 'Constant', value: 2 },
],
},
},
],
},
});
});

it('enforces union guards and self-cycle serialization constraints', function () {
const db: any = firebase.firestore();
const secondaryDb: any = firebase.app('secondaryFromNative').firestore();
Expand Down Expand Up @@ -452,6 +526,10 @@ describe('Firestore pipelines runtime', function () {
.pipeline()
.documents(['firestore/a'])
.select(
arrayFirst(field('items')).as('firstArrayItem'),
arrayFirstN(field('items'), 2).as('firstArrayItems'),
field('items').arrayFirst().as('fluentFirstArrayItem'),
field('items').arrayFirstN(2).as('fluentFirstArrayItems'),
arrayGet(field('items'), 0).as('firstItem'),
conditional(
field('value').greaterThan(0),
Expand All @@ -468,6 +546,8 @@ describe('Firestore pipelines runtime', function () {
.serialize();

expect(getIOSUnsupportedPipelineFunctions(serialized)).toEqual([
'arrayFirst',
'arrayFirstN',
'arrayGet',
'conditional',
'round',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,27 @@ private static final class ExitReceiverArrayGetFrame implements ObjectLoweringFr
}
}

private static final class ExitReceiverArrayFirstNFrame implements ObjectLoweringFrame {
final LoweredExpressionBox box;
final List<PendingReceiverOperation> pendingOperations;
final int nextIndex;
final Expression currentExpression;
final LoweredExpressionBox countBox;

ExitReceiverArrayFirstNFrame(
LoweredExpressionBox box,
List<PendingReceiverOperation> pendingOperations,
int nextIndex,
Expression currentExpression,
LoweredExpressionBox countBox) {
this.box = box;
this.pendingOperations = pendingOperations;
this.nextIndex = nextIndex;
this.currentExpression = currentExpression;
this.countBox = countBox;
}
}

private static final class ExitReceiverArrayConcatFrame implements ObjectLoweringFrame {
final LoweredExpressionBox box;
final List<PendingReceiverOperation> pendingOperations;
Expand Down Expand Up @@ -1556,6 +1577,36 @@ private void processObjectLoweringStack(ArrayDeque<ObjectLoweringFrame> stack)
indexArg, operationFieldName + ".args[1]", indexBox));
continue;
}
case "arrayfirstn":
{
Object countArg = args.get(1);
if (!containsLowerableExpression(countArg)) {
Object countValue = resolveConstantValue(countArg, operationFieldName + ".args[1]");
if (countValue instanceof Number) {
stack.push(
new ContinueReceiverExpressionChainFrame(
continueFrame.box,
null,
continueFrame.pendingOperations,
nextIndex,
currentExpression.arrayFirstN(((Number) countValue).intValue())));
continue;
}
}

LoweredExpressionBox countBox = new LoweredExpressionBox();
stack.push(
new ExitReceiverArrayFirstNFrame(
continueFrame.box,
continueFrame.pendingOperations,
nextIndex,
currentExpression,
countBox));
stack.push(
new EnterObjectExpressionValueFrame(
countArg, operationFieldName + ".args[1]", countBox));
continue;
}
case "arrayconcat":
{
if (args.size() < 2) {
Expand Down Expand Up @@ -1787,6 +1838,18 @@ private void processObjectLoweringStack(ArrayDeque<ObjectLoweringFrame> stack)
continue;
}

if (frame instanceof ExitReceiverArrayFirstNFrame) {
ExitReceiverArrayFirstNFrame exitFrame = (ExitReceiverArrayFirstNFrame) frame;
stack.push(
new ContinueReceiverExpressionChainFrame(
exitFrame.box,
null,
exitFrame.pendingOperations,
exitFrame.nextIndex,
exitFrame.currentExpression.arrayFirstN(exitFrame.countBox.value)));
continue;
}

if (frame instanceof ExitReceiverArrayConcatFrame) {
ExitReceiverArrayConcatFrame exitFrame = (ExitReceiverArrayConcatFrame) frame;
Object secondValue = exitFrame.childBoxes.get(0).value;
Expand Down Expand Up @@ -2571,6 +2634,7 @@ private boolean isDeferredUnaryExpressionFunction(String normalizedFunctionName)
return "type".equals(normalizedFunctionName)
|| "collectionid".equals(normalizedFunctionName)
|| "documentid".equals(normalizedFunctionName)
|| "arrayfirst".equals(normalizedFunctionName)
|| "arraylength".equals(normalizedFunctionName)
|| "arraysum".equals(normalizedFunctionName)
|| "vectorlength".equals(normalizedFunctionName)
Expand All @@ -2588,6 +2652,7 @@ private boolean isDeferredReceiverExpressionFunction(String normalizedFunctionNa
|| "mapget".equals(normalizedFunctionName)
|| "mapmerge".equals(normalizedFunctionName)
|| "arrayget".equals(normalizedFunctionName)
|| "arrayfirstn".equals(normalizedFunctionName)
|| "arrayconcat".equals(normalizedFunctionName)
|| "cosinedistance".equals(normalizedFunctionName)
|| "dotproduct".equals(normalizedFunctionName)
Expand All @@ -2612,6 +2677,9 @@ private Expression applyPendingUnaryExpressionFunctions(
case "documentid":
currentExpression = currentExpression.documentId();
break;
case "arrayfirst":
currentExpression = currentExpression.arrayFirst();
break;
case "arraylength":
currentExpression = currentExpression.arrayLength();
break;
Expand Down Expand Up @@ -2758,6 +2826,12 @@ private Expression buildSpecialParsedExpressionFunction(
case "arrayget":
requireParsedArgumentCount(args, 2, functionName, fieldName);
return buildParsedArrayGetExpression(args, fieldName);
case "arrayfirst":
requireParsedArgumentCount(args, 1, functionName, fieldName);
return coerceExpressionValueNode(args.get(0), fieldName + ".args[0]").arrayFirst();
case "arrayfirstn":
requireParsedArgumentCount(args, 2, functionName, fieldName);
return buildParsedArrayFirstNExpression(args, fieldName);
case "arrayconcat":
return buildParsedArrayConcatExpression(args, functionName, fieldName);
case "arraysum":
Expand Down Expand Up @@ -2975,6 +3049,19 @@ private Expression buildParsedArrayGetExpression(
return arrayExpr.arrayGet(coerceExpressionValueNode(args.get(1), fieldName + ".args[1]"));
}

private Expression buildParsedArrayFirstNExpression(
List<ReactNativeFirebaseFirestorePipelineParser.ParsedValueNode> args, String fieldName)
throws ReactNativeFirebaseFirestorePipelineExecutor.PipelineValidationException {
Expression arrayExpr = coerceExpressionValueNode(args.get(0), fieldName + ".args[0]");
if (!containsParsedExpression(args.get(1))) {
Object countValue = resolveValueNode(args.get(1), fieldName + ".args[1]");
if (countValue instanceof Number) {
return arrayExpr.arrayFirstN(((Number) countValue).intValue());
}
}
return arrayExpr.arrayFirstN(coerceExpressionValueNode(args.get(1), fieldName + ".args[1]"));
}

private Expression buildParsedArrayConcatExpression(
List<ReactNativeFirebaseFirestorePipelineParser.ParsedValueNode> args,
String functionName,
Expand Down
17 changes: 17 additions & 0 deletions packages/firestore/consumer-type-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ import {
// array
array,
arrayFilter,
arrayFirst,
arrayFirstN,
arrayConcat,
arrayGet,
arrayLength,
Expand Down Expand Up @@ -1318,6 +1320,17 @@ void variable('score');
void arrayFilter('scores', 'score', greaterThan(variable('score'), constant(15)));
void arrayFilter(field('scores'), 'score', greaterThan(variable('score'), constant(15)));
void field('scores').arrayFilter('score', greaterThan(variable('score'), constant(15)));
// arrayFirst: (string) | (Expression)
void arrayFirst('items');
void arrayFirst(field('items'));
// arrayFirstN: 4 overloads
void arrayFirstN('items', 2);
void arrayFirstN('items', field('limit'));
void arrayFirstN(field('items'), 2);
void arrayFirstN(field('items'), field('limit'));
void field('items').arrayFirst();
void field('items').arrayFirstN(2);
void field('items').arrayFirstN(field('limit'));
// arrayConcat: (Expression, ...) | (string, ...)
void arrayConcat(field('tags'), field('moreTags'));
void arrayConcat(field('tags'), ['extra']);
Expand Down Expand Up @@ -1692,6 +1705,10 @@ const pipelineArrayOps = xDb
array([constant(1), constant(2), constant(3)]).as('fixedArr'),
arrayLength(field('comments')).as('commentCount'),
arrayLength('comments').as('commentCount2'),
arrayFirst(field('items')).as('firstItemByHelper'),
arrayFirst('items').as('firstItemByField'),
arrayFirstN(field('items'), 2).as('firstItems'),
arrayFirstN('items', field('limit')).as('dynamicFirstItems'),
arrayGet(field('items'), 0).as('firstItem'),
arrayGet(field('items'), field('idx')).as('dynamicItem'),
arrayGet('items', 0).as('firstItem2'),
Expand Down
14 changes: 12 additions & 2 deletions packages/firestore/e2e/Pipeline.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -1510,13 +1510,15 @@ describe('FirestorePipeline', function () {
});

describe('array operators', function () {
it('evaluates array, arrayLength, arrayGet, arrayConcat, arraySum and array predicates', async function () {
it('evaluates array helpers and array predicates', async function () {
const {
execute,
field,
constant,
array,
arrayLength,
arrayFirst,
arrayFirstN,
arrayGet,
arrayConcat,
arrayFilter,
Expand Down Expand Up @@ -1564,6 +1566,8 @@ describe('FirestorePipeline', function () {
.select(
array([constant(1), constant(2), constant(3)]).as('fixedArr'),
arrayLength(field('tags')).as('tagCount'),
arrayFirst(field('items')).as('firstItemByHelper'),
arrayFirstN(field('items'), 2).as('firstTwoItems'),
arrayGet(field('items'), 0).as('firstItem'),
arrayConcat(field('primaryTags'), field('secondaryTags')).as('allTags'),
arrayFilter(field('scores'), 'score', greaterThan(variable('score'), 15)).as(
Expand All @@ -1573,7 +1577,11 @@ describe('FirestorePipeline', function () {
);

if (Platform.ios) {
await expectIOSUnsupportedFunctions(() => execute(pipeline), ['arrayGet']);
await expectIOSUnsupportedFunctions(() => execute(pipeline), [
'arrayFirst',
'arrayFirstN',
'arrayGet',
]);

const iosSnapshot = await execute(
db
Expand Down Expand Up @@ -1613,6 +1621,8 @@ describe('FirestorePipeline', function () {
const data = snapshot.results[0].data();
data.fixedArr.should.eql([1, 2, 3]);
data.tagCount.should.equal(2);
data.firstItemByHelper.should.equal('x');
data.firstTwoItems.should.eql(['x', 'y']);
data.firstItem.should.equal('x');
data.allTags.should.eql(['a', 'b', 'c', 'd']);
data.filteredItems.should.eql([20, 30]);
Expand Down
Loading
Loading