Skip to content

Commit

Permalink
db: add support for has and !has set operators
Browse files Browse the repository at this point in the history
  • Loading branch information
pbohlman committed Mar 1, 2024
1 parent 1294dfd commit b07bba6
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/mighty-llamas-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@triplit/db': patch
---

Ensure that schemas passed in to the DB constructor have id and collection triples
5 changes: 5 additions & 0 deletions .changeset/sour-fireants-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@triplit/db': patch
---

Add support for 'has' and '!has' set operators
26 changes: 16 additions & 10 deletions packages/db/src/collection-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,14 +445,15 @@ function* generateQueryChains<

const REVERSE_OPERATOR_MAPPINGS = {
'=': '=',
'!=': '!=',
'<': '>',
'>': '<',
'<=': '>=',
'>=': '<=',
in: '=', // we need the inverse for sets
// TODO support Set operators
// the issue is that we use '=' for set membership so we can't just reverse the operator
// naively to "in"
in: 'has',
nin: '!has',
has: 'in',
'!has': 'nin',
};
function reverseRelationFilter(filter: FilterStatement<any, any>) {
const [path, op, value] = filter;
Expand Down Expand Up @@ -873,13 +874,18 @@ function satisfiesSetFilter(
return false;
}
}

const setData = timestampedObjectToPlainObject(value);
return (
setData &&
Object.entries(setData)
.filter(([_v, inSet]) => inSet)
.some(([v]) => isOperatorSatisfied(op, v, filterValue))
);
if (!setData) return false;
const filteredSet = Object.entries(setData).filter(([_v, inSet]) => inSet);
if (op === 'has') {
return filteredSet.some(([v]) => v === filterValue);
}
if (op === '!has') {
return filteredSet.every(([v]) => v !== filterValue);
}

return filteredSet.some(([v]) => isOperatorSatisfied(op, v, filterValue));
}

function satisfiesRegisterFilter(
Expand Down
4 changes: 3 additions & 1 deletion packages/db/src/data-types/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export type Operator =
| 'like'
| 'nlike'
| 'in'
| 'nin';
| 'nin'
| 'has'
| '!has';

export type Optional<T extends DataType> = T & { context: { optional: true } };

Expand Down
2 changes: 1 addition & 1 deletion packages/db/src/data-types/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { ExtractJSType } from './type.js';
import { ChangeTracker } from '../db-transaction.js';
import { TypeWithOptions } from './value.js';

const SET_OPERATORS = ['=', '!='] as const;
const SET_OPERATORS = ['=', '!=', 'has', '!has'] as const;
type SetOperators = typeof SET_OPERATORS;

export type SetType<
Expand Down
54 changes: 54 additions & 0 deletions packages/db/test/db.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,60 @@ describe('Database API', () => {
{ name: "Travis 'LaFlame' Scott", id: '6', rank: 6 },
];

it.only('supports basic queries with the has and !has operators', async () => {
const db = new DB({
schema: {
collections: {
Classes: {
schema: S.Schema({
id: S.Id(),
name: S.String(),
level: S.Number(),
department: S.String(),
enrolled_students: S.Set(S.String()),
}),
},
},
},
});
await Promise.all(
classes.map((cls) =>
db.insert('Classes', {
...cls,
enrolled_students: new Set(cls.enrolled_students),
})
)
);
const results = await db.fetch(
CollectionQueryBuilder('Classes')
.where([['enrolled_students', 'has', 'student-1']])
.build()
);
expect([...results.keys()]).toStrictEqual(['class-2', 'class-3']);
const results2 = await db.fetch(
CollectionQueryBuilder('Classes')
.where([['enrolled_students', 'has', 'bad-id']])
.build()
);
expect(results2.size).toBe(0);
const results3 = await db.fetch(
CollectionQueryBuilder('Classes')
.where([['enrolled_students', '!has', 'student-1']])
.build()
);
expect([...results3.keys()]).toStrictEqual([
'class-1',
'class-4',
'class-5',
]);
const results4 = await db.fetch(
CollectionQueryBuilder('Classes')
.where([['enrolled_students', '!has', 'bad-id']])
.build()
);
expect(results4.size).toBe(5);
});

it('supports basic queries without filters', async () => {
const results = await db.fetch(CollectionQueryBuilder('Student').build());
expect(results.size).toBe(students.length);
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/src/pages/schemas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ import { Schema as S } from '@triplit/db';
const stringSet = S.Set(S.String());
```

Sets support `=`, `!=` operators in `where` statements, which check if the set contains the value.
Sets support `has` and `!has` operators in `where` statements, which check if the set does or does not contain the value.

### Record

Expand Down

0 comments on commit b07bba6

Please sign in to comment.