Skip to content
Open
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
110 changes: 110 additions & 0 deletions packages/db/tests/collection-indexes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,19 @@ describe(`Collection Indexes`, () => {
})
})

it(`should exclude the boundary value from greater than queries on dates`, () => {
// gt must be strict for date fields: Bob was created exactly on
// 2023-01-02, so only rows created strictly later may be returned.
collection.createIndex((row) => row.createdAt)

const result = collection.currentStateAsChanges({
where: gt(new PropRef([`createdAt`]), new Date(`2023-01-02`)),
})!

const names = result.map((r) => r.value.name).sort()
expect(names).toEqual([`Charlie`, `Diana`, `Eve`])
})

it(`should perform greater than or equal queries`, () => {
withIndexTracking(collection, (tracker) => {
const result = collection.currentStateAsChanges({
Expand Down Expand Up @@ -1179,6 +1192,103 @@ describe(`Collection Indexes`, () => {
})
})
})

it(`should include rows matched by any OR condition when conditions mix indexed and non-indexed expressions`, () => {
// An OR query must return the union of rows matching each condition:
// eq(age, 25) matches Alice (age 25)
// gt(length(name), 6) matches Charlie (name length 7)
// `age` has an index while `length(name)` is a computed expression
// without one, but the chosen execution strategy must not change the
// result: both Alice and Charlie satisfy the OR and must be returned.
const result = collection.currentStateAsChanges({
where: or(
eq(new PropRef([`age`]), 25),
gt(length(new PropRef([`name`])), 6),
),
})!

const names = result.map((r) => r.value.name).sort()
expect(names).toEqual([`Alice`, `Charlie`])
})

it(`should only return rows matching every AND condition when conditions mix indexed and non-indexed expressions`, () => {
// An AND query must return only the rows matching all conditions:
// eq(status, 'active') matches Alice, Charlie and Eve
// gt(length(name), 6) matches only Charlie (name length 7)
// `status` has an index while `length(name)` is a computed expression
// without one, but every condition must still be enforced: only
// Charlie satisfies both.
const result = collection.currentStateAsChanges({
where: and(
eq(new PropRef([`status`]), `active`),
gt(length(new PropRef([`name`])), 6),
),
})!

const names = result.map((r) => r.value.name).sort()
expect(names).toEqual([`Charlie`])
})

it(`should apply the strictest lower bound when range conditions share the same value`, () => {
// gte(age, 25) AND gt(age, 25) reduces to age > 25: the strict
// comparison wins at the shared boundary, so Alice (age 25) must be
// excluded regardless of the order the conditions appear in.
const result = collection.currentStateAsChanges({
where: and(gte(new PropRef([`age`]), 25), gt(new PropRef([`age`]), 25)),
})!

const names = result.map((r) => r.value.name).sort()
expect(names).toEqual([`Bob`, `Charlie`, `Diana`])
})

it(`should apply the strictest upper bound when range conditions share the same value`, () => {
// lte(age, 30) AND lt(age, 30) reduces to age < 30: the strict
// comparison wins at the shared boundary, so Bob (age 30) must be
// excluded.
const result = collection.currentStateAsChanges({
where: and(lte(new PropRef([`age`]), 30), lt(new PropRef([`age`]), 30)),
})!

const names = result.map((r) => r.value.name).sort()
expect(names).toEqual([`Alice`, `Diana`, `Eve`])
})

it(`should apply the strictest bound for date ranges sharing the same value`, () => {
// Distinct Date instances representing the same point in time must be
// treated as equal values: gte(createdAt, jan2) AND gt(createdAt, jan2)
// reduces to createdAt > jan2, so Bob (created 2023-01-02) must be
// excluded.
collection.createIndex((row) => row.createdAt)

const result = collection.currentStateAsChanges({
where: and(
gte(new PropRef([`createdAt`]), new Date(`2023-01-02`)),
gt(new PropRef([`createdAt`]), new Date(`2023-01-02`)),
),
})!

const names = result.map((r) => r.value.name).sort()
expect(names).toEqual([`Charlie`, `Diana`, `Eve`])
})

it(`should enforce every AND condition when a range on one field is combined with conditions on other fields`, () => {
// An AND query that contains a compound range on one field plus a
// condition on another field must enforce all of them:
// gt(age, 24) AND lt(age, 36) matches Alice (25), Bob (30),
// Charlie (35) and Diana (28)
// eq(status, 'active') matches Alice, Charlie and Eve
// Only Alice and Charlie satisfy the full conjunction.
const result = collection.currentStateAsChanges({
where: and(
gt(new PropRef([`age`]), 24),
lt(new PropRef([`age`]), 36),
eq(new PropRef([`status`]), `active`),
),
})!

const names = result.map((r) => r.value.name).sort()
expect(names).toEqual([`Alice`, `Charlie`])
})
})

describe(`Index Usage Verification`, () => {
Expand Down
Loading