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
2 changes: 1 addition & 1 deletion src/packages/pongo/src/core/collection/filters/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const asPlainObjectWithSingleKey = <T>(
!Array.isArray(filter) &&
Object.keys(filter).length === 1 &&
key in filter
? filter
? { [key]: (filter as Record<string, unknown>)[key] }
: undefined;

export const idFromFilter = <T>(
Expand Down
13 changes: 7 additions & 6 deletions src/packages/pongo/src/core/typing/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,12 +407,6 @@ export declare type Condition<T> =
| AlternativeType<T>
| PongoFilterOperator<AlternativeType<T>>;

export declare type PongoFilter<TSchema> =
| {
[P in keyof WithId<TSchema>]?: Condition<WithId<TSchema>[P]>;
}
| HasId; // TODO: & RootFilterOperators<WithId<TSchema>>;

export declare interface RootFilterOperators<TSchema> extends Document {
$and?: PongoFilter<TSchema>[];
$nor?: PongoFilter<TSchema>[];
Expand All @@ -427,6 +421,13 @@ export declare interface RootFilterOperators<TSchema> extends Document {
$comment?: string | Document;
}

export declare type PongoFilter<TSchema> =
| ({
[P in keyof WithId<TSchema>]?: Condition<WithId<TSchema>[P]>;
} & Pick<RootFilterOperators<WithId<TSchema>>, '$and' | '$nor' | '$or'>)
| (HasId &
Pick<RootFilterOperators<WithId<TSchema>>, '$and' | '$nor' | '$or'>);

export declare interface PongoFilterOperator<
TValue,
> extends NonObjectIdLikeDocument {
Expand Down
104 changes: 104 additions & 0 deletions src/packages/pongo/src/e2e/postgresql/pg/postgres.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,110 @@ describe('MongoDB Compatibility Tests', () => {
);
});

it('should find documents with a top-level $or filter', async () => {
const pongoCollection = pongoDb.collection<User>('findWithTopLevelOr');
const mongoCollection = mongoDb.collection<User>(
'shimfindWithTopLevelOr',
);
const docs = [
{ name: 'David', age: 40 },
{ name: 'Eve', age: 45 },
{ name: 'Frank', age: 50 },
];

await pongoCollection.insertOne(docs[0]!);
await pongoCollection.insertOne(docs[1]!);
await pongoCollection.insertOne(docs[2]!);

await mongoCollection.insertOne(docs[0]!);
await mongoCollection.insertOne(docs[1]!);
await mongoCollection.insertOne(docs[2]!);

const pongoDocs = await pongoCollection.find({
$or: [{ age: 40 }, { age: 50 }],
});
const mongoDocs = await mongoCollection
.find({
$or: [{ age: 40 }, { age: 50 }],
})
.toArray();

assert.deepStrictEqual(
pongoDocs.map((d) => ({ name: d.name, age: d.age })),
mongoDocs.map((d) => ({ name: d.name, age: d.age })),
);
});

it('should find documents with nested $and and $or filters', async () => {
const pongoCollection = pongoDb.collection<User>(
'findWithNestedLogicalOperators',
);
const mongoCollection = mongoDb.collection<User>(
'shimfindWithNestedLogicalOperators',
);
const docs = [
{ name: 'Anita', age: 25 },
{ name: 'Roger', age: 30 },
{ name: 'Cruella', age: 35 },
];

await pongoCollection.insertOne(docs[0]!);
await pongoCollection.insertOne(docs[1]!);
await pongoCollection.insertOne(docs[2]!);

await mongoCollection.insertOne(docs[0]!);
await mongoCollection.insertOne(docs[1]!);
await mongoCollection.insertOne(docs[2]!);

const pongoDocs = await pongoCollection.find({
$and: [{ age: { $gte: 30 } }, { $or: [{ name: 'Roger' }] }],
});
const mongoDocs = await mongoCollection
.find({
$and: [{ age: { $gte: 30 } }, { $or: [{ name: 'Roger' }] }],
})
.toArray();

assert.deepStrictEqual(
pongoDocs.map((d) => ({ name: d.name, age: d.age })),
mongoDocs.map((d) => ({ name: d.name, age: d.age })),
);
});

it('should find documents with a top-level $nor filter', async () => {
const pongoCollection = pongoDb.collection<User>('findWithTopLevelNor');
const mongoCollection = mongoDb.collection<User>(
'shimfindWithTopLevelNor',
);
const docs = [
{ name: 'Anita', age: 25 },
{ name: 'Roger', age: 30 },
{ name: 'Cruella', age: 35 },
];

await pongoCollection.insertOne(docs[0]!);
await pongoCollection.insertOne(docs[1]!);
await pongoCollection.insertOne(docs[2]!);

await mongoCollection.insertOne(docs[0]!);
await mongoCollection.insertOne(docs[1]!);
await mongoCollection.insertOne(docs[2]!);

const pongoDocs = await pongoCollection.find({
$nor: [{ age: 25 }, { age: 35 }],
});
const mongoDocs = await mongoCollection
.find({
$nor: [{ age: 25 }, { age: 35 }],
})
.toArray();

assert.deepStrictEqual(
pongoDocs.map((d) => ({ name: d.name, age: d.age })),
mongoDocs.map((d) => ({ name: d.name, age: d.age })),
);
});

it('should find one document with a filter', async () => {
const pongoCollection = pongoDb.collection<User>('testCollection');
const mongoCollection = mongoDb.collection<User>('shimtestCollection');
Expand Down
104 changes: 104 additions & 0 deletions src/packages/pongo/src/e2e/sqlite/sqlite3/sqlite3.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,110 @@ describe('SQLite MongoDB Compatibility Tests', () => {
);
});

it('should find documents with a top-level $or filter', async () => {
const pongoCollection = pongoDb.collection<User>('findWithTopLevelOr');
const mongoCollection = mongoDb.collection<User>(
'shimfindWithTopLevelOr',
);
const docs = [
{ name: 'David', age: 40 },
{ name: 'Eve', age: 45 },
{ name: 'Frank', age: 50 },
];

await pongoCollection.insertOne(docs[0]!);
await pongoCollection.insertOne(docs[1]!);
await pongoCollection.insertOne(docs[2]!);

await mongoCollection.insertOne(docs[0]!);
await mongoCollection.insertOne(docs[1]!);
await mongoCollection.insertOne(docs[2]!);

const pongoDocs = await pongoCollection.find({
$or: [{ age: 40 }, { age: 50 }],
});
const mongoDocs = await mongoCollection
.find({
$or: [{ age: 40 }, { age: 50 }],
})
.toArray();

assert.deepStrictEqual(
pongoDocs.map((d) => ({ name: d.name, age: d.age })),
mongoDocs.map((d) => ({ name: d.name, age: d.age })),
);
});

it('should find documents with nested $and and $or filters', async () => {
const pongoCollection = pongoDb.collection<User>(
'findWithNestedLogicalOperators',
);
const mongoCollection = mongoDb.collection<User>(
'shimfindWithNestedLogicalOperators',
);
const docs = [
{ name: 'Anita', age: 25 },
{ name: 'Roger', age: 30 },
{ name: 'Cruella', age: 35 },
];

await pongoCollection.insertOne(docs[0]!);
await pongoCollection.insertOne(docs[1]!);
await pongoCollection.insertOne(docs[2]!);

await mongoCollection.insertOne(docs[0]!);
await mongoCollection.insertOne(docs[1]!);
await mongoCollection.insertOne(docs[2]!);

const pongoDocs = await pongoCollection.find({
$and: [{ age: { $gte: 30 } }, { $or: [{ name: 'Roger' }] }],
});
const mongoDocs = await mongoCollection
.find({
$and: [{ age: { $gte: 30 } }, { $or: [{ name: 'Roger' }] }],
})
.toArray();

assert.deepStrictEqual(
pongoDocs.map((d) => ({ name: d.name, age: d.age })),
mongoDocs.map((d) => ({ name: d.name, age: d.age })),
);
});

it('should find documents with a top-level $nor filter', async () => {
const pongoCollection = pongoDb.collection<User>('findWithTopLevelNor');
const mongoCollection = mongoDb.collection<User>(
'shimfindWithTopLevelNor',
);
const docs = [
{ name: 'Anita', age: 25 },
{ name: 'Roger', age: 30 },
{ name: 'Cruella', age: 35 },
];

await pongoCollection.insertOne(docs[0]!);
await pongoCollection.insertOne(docs[1]!);
await pongoCollection.insertOne(docs[2]!);

await mongoCollection.insertOne(docs[0]!);
await mongoCollection.insertOne(docs[1]!);
await mongoCollection.insertOne(docs[2]!);

const pongoDocs = await pongoCollection.find({
$nor: [{ age: 25 }, { age: 35 }],
});
const mongoDocs = await mongoCollection
.find({
$nor: [{ age: 25 }, { age: 35 }],
})
.toArray();

assert.deepStrictEqual(
pongoDocs.map((d) => ({ name: d.name, age: d.age })),
mongoDocs.map((d) => ({ name: d.name, age: d.age })),
);
});

it('should find one document with a filter', async () => {
const pongoCollection = pongoDb.collection<User>('testCollection');
const mongoCollection = mongoDb.collection<User>('shimtestCollection');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,108 @@ import { handleOperator } from './queryOperators';
export * from './queryOperators';

const AND = 'AND';
const OR = 'OR';

const unsupportedRootOperators = ['$text', '$where', '$comment'] as const;

export const constructFilterQuery = <T>(
filter: PongoFilter<T>,
serializer: JSONSerializer,
): SQL => {
ensureSupportedRootOperators(filter);
const parts: SQL[] = [];

const fieldFilterQuery = constructFieldFilterQuery(filter, serializer);
if (!SQL.check.isEmpty(fieldFilterQuery)) {
parts.push(fieldFilterQuery);
}

const orFilterQuery = constructLogicalFilterQuery(filter.$or, OR, serializer);
if (!SQL.check.isEmpty(orFilterQuery)) {
parts.push(orFilterQuery);
}

const andFilterQuery = constructLogicalFilterQuery(
filter.$and,
AND,
serializer,
);
if (!SQL.check.isEmpty(andFilterQuery)) {
parts.push(andFilterQuery);
}

const norFilterQuery = constructNorFilterQuery(filter.$nor, serializer);
if (!SQL.check.isEmpty(norFilterQuery)) {
parts.push(norFilterQuery);
}

return SQL.merge(parts, ` ${AND} `);
};

const constructFieldFilterQuery = <T>(
filter: PongoFilter<T>,
serializer: JSONSerializer,
): SQL =>
SQL.merge(
Object.entries(filter).map(([key, value]) =>
isRecord(value)
? constructComplexFilterQuery(key, value, serializer)
: handleOperator(key, '$eq', value, serializer),
objectEntries(filter).flatMap(([key, value]) =>
isLogicalRootOperator(key)
? []
: [
isRecord(value)
? constructComplexFilterQuery(key, value, serializer)
: handleOperator(key, QueryOperators.$eq, value, serializer),
],
),
` ${AND} `,
);

const constructLogicalFilterQuery = <T>(
filters: PongoFilter<T>[] | undefined,
joinOperator: typeof AND | typeof OR,
serializer: JSONSerializer,
): SQL => {
if (!filters?.length) {
return SQL.EMPTY;
}

const subFilterQueries = filters.reduce<SQL[]>((queries, filter) => {
const query = constructFilterQuery(filter, serializer);
if (!SQL.check.isEmpty(query)) {
queries.push(query);
}

return queries;
}, []);

if (subFilterQueries.length === 0) {
return SQL.EMPTY;
}

if (subFilterQueries.length === 1) {
return wrapFilterQuery(subFilterQueries[0]!);
}

return SQL`(${SQL.merge(subFilterQueries.map(wrapFilterQuery), ` ${joinOperator} `)})`;
};

const constructNorFilterQuery = <T>(
filters: PongoFilter<T>[] | undefined,
serializer: JSONSerializer,
): SQL => {
if (!filters?.length) {
return SQL.EMPTY;
}

const logicalFilterQuery = constructLogicalFilterQuery(
filters,
OR,
serializer,
);
return SQL.check.isEmpty(logicalFilterQuery)
? SQL.EMPTY
: SQL`NOT ${logicalFilterQuery}`;
};

const constructComplexFilterQuery = (
key: string,
value: Record<string, unknown>,
Expand All @@ -47,5 +135,18 @@ const constructComplexFilterQuery = (
);
};

const wrapFilterQuery = (filterQuery: SQL): SQL => SQL`(${filterQuery})`;

const ensureSupportedRootOperators = (filter: object): void => {
for (const operator of unsupportedRootOperators) {
if (operator in filter) {
throw new Error(`Unsupported root operator: ${operator}`);
}
}
};

const isLogicalRootOperator = (key: string): key is '$and' | '$nor' | '$or' =>
key === '$and' || key === '$nor' || key === '$or';

const isRecord = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value);
Loading