Skip to content

Commit

Permalink
Created unit tests for the filter utils (FilterH reductor).
Browse files Browse the repository at this point in the history
  • Loading branch information
TPReal committed Dec 14, 2023
1 parent 1a9edc9 commit 23b4166
Show file tree
Hide file tree
Showing 2 changed files with 251 additions and 2 deletions.
242 changes: 242 additions & 0 deletions resources/js/data-access/memo-api/tquery/filter_utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import {describe, expect, test} from "vitest";
import {FilterH, FilterReductor, invert} from "./filter_utils";

describe("filter_utils", () => {
const reductor = new FilterReductor({
columns: [
{name: "c-str", type: "string"},
{name: "c-nullable-str", type: "string", nullable: true},
{name: "c-uuid-list", type: "uuid_list", nullable: true},
],
});

test("invert", () => {
expect(invert({type: "column", column: "foo", op: "=", val: "bar"})).toEqual({
type: "column",
column: "foo",
op: "=",
val: "bar",
inv: true,
});
expect(invert({type: "column", column: "foo", op: "=", val: "bar", inv: true})).toEqual({
type: "column",
column: "foo",
op: "=",
val: "bar",
inv: false,
});
expect(invert("always")).toEqual("never");
expect(invert("never")).toEqual("always");
});

test("op null", () => {
expect(reductor.reduce({type: "column", column: "c-str", op: "null"})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-nullable-str", op: "null"})).toEqual({
type: "column",
column: "c-nullable-str",
op: "null",
});
});

test("empty string val", () => {
expect(reductor.reduce({type: "column", column: "c-str", op: "=", val: ""})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-str", op: ">", val: ""})).toEqual("always");
expect(reductor.reduce({type: "column", column: "c-str", op: "<", val: ""})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-str", op: ">=", val: ""})).toEqual("always");
expect(reductor.reduce({type: "column", column: "c-str", op: "<=", val: ""})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-str", op: "v%", val: ""})).toEqual("always");

const nullFilter = {type: "column", column: "c-nullable-str", op: "null"};
const notNullFilter = {...nullFilter, inv: true};
expect(reductor.reduce({type: "column", column: "c-nullable-str", op: "=", val: ""})).toEqual(nullFilter);
expect(reductor.reduce({type: "column", column: "c-nullable-str", op: ">", val: ""})).toEqual(notNullFilter);
expect(reductor.reduce({type: "column", column: "c-nullable-str", op: "<", val: ""})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-nullable-str", op: ">=", val: ""})).toEqual("always");
expect(reductor.reduce({type: "column", column: "c-nullable-str", op: "<=", val: ""})).toEqual(nullFilter);
expect(reductor.reduce({type: "column", column: "c-nullable-str", op: "v%", val: ""})).toEqual("always");
});

test("untrimmed string val", () => {
expect(reductor.reduce({type: "column", column: "c-str", op: "=", val: "foo "})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-str", op: "<", val: "foo "})).toEqual({
type: "column",
column: "c-str",
op: "<",
val: "foo ",
});

expect(reductor.reduce({type: "column", column: "c-nullable-str", op: "=", val: "foo "})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-nullable-str", op: "<", val: "foo "})).toEqual({
type: "column",
column: "c-nullable-str",
op: "<",
val: "foo ",
});
});

describe("has_* filters", () => {
test("empty val", () => {
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_all", val: []})).toEqual("always");
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_any", val: []})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_only", val: []})).toEqual({
type: "column",
column: "c-uuid-list",
op: "null",
});
});

test("single element in val", () => {
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_all", val: ["x"]})).toEqual({
type: "column",
column: "c-uuid-list",
op: "has",
val: "x",
});
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_any", val: ["x"]})).toEqual({
type: "column",
column: "c-uuid-list",
op: "has",
val: "x",
});
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_only", val: ["x"]})).toEqual({
type: "column",
column: "c-uuid-list",
op: "has_only",
val: ["x"],
});
});

test("empty string in val", () => {
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_all", val: [""]})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_any", val: [""]})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_only", val: [""]})).toEqual({
type: "column",
column: "c-uuid-list",
op: "null",
});

expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_all", val: ["", "x"]})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_any", val: ["", "x"]})).toEqual({
type: "column",
column: "c-uuid-list",
op: "has",
val: "x",
});
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_only", val: ["", "x"]})).toEqual({
type: "column",
column: "c-uuid-list",
op: "has_only",
val: ["x"],
});
});

test("untrimmed string in val", () => {
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_all", val: ["x "]})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_any", val: ["x "]})).toEqual("never");
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_only", val: ["x "]})).toEqual({
type: "column",
column: "c-uuid-list",
op: "null",
});

expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_all", val: ["x ", "y"]})).toEqual(
"never",
);
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_any", val: ["x ", "y"]})).toEqual({
type: "column",
column: "c-uuid-list",
op: "has",
val: "y",
});
expect(reductor.reduce({type: "column", column: "c-uuid-list", op: "has_only", val: ["x ", "y"]})).toEqual({
type: "column",
column: "c-uuid-list",
op: "has_only",
val: ["y"],
});
});
});

describe("bool op filters", () => {
test("empty val", () => {
expect(reductor.reduce({type: "op", op: "&", val: []})).toEqual("always");
expect(reductor.reduce({type: "op", op: "|", val: []})).toEqual("never");
});

test("single element in val", () => {
const f: FilterH = {type: "column", column: "c-uuid-list", op: "has_all", val: ["x", "y", "z"]};
expect(reductor.reduce({type: "op", op: "&", val: [f]})).toEqual(f);
expect(reductor.reduce({type: "op", op: "|", val: [f]})).toEqual(f);
});

test("always/never in val", () => {
const alwaysFilter: FilterH = {type: "column", column: "c-str", op: "v%", val: ""};
const neverFilter: FilterH = {type: "column", column: "c-str", op: "v%", val: "", inv: true};
expect(reductor.reduce(alwaysFilter)).toEqual("always");
expect(reductor.reduce(neverFilter)).toEqual("never");

const f: FilterH = {type: "column", column: "c-uuid-list", op: "has_all", val: ["x", "y", "z"]};
expect(reductor.reduce({type: "op", op: "&", val: [alwaysFilter, f]})).toEqual(f);
expect(reductor.reduce({type: "op", op: "&", val: [neverFilter, f]})).toEqual("never");
expect(reductor.reduce({type: "op", op: "&", val: [alwaysFilter, neverFilter, f]})).toEqual("never");
expect(reductor.reduce({type: "op", op: "|", val: [alwaysFilter, f]})).toEqual("always");
expect(reductor.reduce({type: "op", op: "|", val: [neverFilter, f]})).toEqual(f);
expect(reductor.reduce({type: "op", op: "|", val: [alwaysFilter, neverFilter, f]})).toEqual("always");

expect(reductor.reduce({type: "op", op: "&", val: [alwaysFilter, f], inv: true})).toEqual(invert(f));
expect(reductor.reduce({type: "op", op: "&", val: [neverFilter, f], inv: true})).toEqual("always");
expect(reductor.reduce({type: "op", op: "&", val: [alwaysFilter, neverFilter, f], inv: true})).toEqual("always");
expect(reductor.reduce({type: "op", op: "|", val: [alwaysFilter, f], inv: true})).toEqual("never");
expect(reductor.reduce({type: "op", op: "|", val: [neverFilter, f], inv: true})).toEqual(invert(f));
expect(reductor.reduce({type: "op", op: "|", val: [alwaysFilter, neverFilter, f], inv: true})).toEqual("never");
});

test("nested bool op filters", () => {
const f: FilterH = {type: "column", column: "c-uuid-list", op: "has_all", val: ["x", "y", "z"]};
const g: FilterH = {type: "column", column: "c-str", op: "v%", val: "foo"};
const h: FilterH = {type: "column", column: "c-nullable-str", op: "=", val: "bar"};

expect(
reductor.reduce({
type: "op",
op: "&",
val: [
{type: "op", op: "&", val: [f, g]},
{type: "op", op: "&", val: [h]},
],
}),
).toEqual({type: "op", op: "&", val: [f, g, h]});
expect(
reductor.reduce({
type: "op",
op: "|",
val: [
{type: "op", op: "|", val: [f, g]},
{type: "op", op: "|", val: [h]},
],
}),
).toEqual({type: "op", op: "|", val: [f, g, h]});

expect(
reductor.reduce({
type: "op",
op: "&",
val: [
{type: "op", op: "|", val: [f, g], inv: true},
{type: "op", op: "|", val: [h], inv: true},
],
}),
).toEqual({type: "op", op: "&", val: [invert(f), invert(g), invert(h)]});
expect(
reductor.reduce({
type: "op",
op: "|",
val: [
{type: "op", op: "&", val: [f, g], inv: true},
{type: "op", op: "&", val: [h], inv: true},
],
}),
).toEqual({type: "op", op: "|", val: [invert(f), invert(g), invert(h)]});
});
});
});
11 changes: 9 additions & 2 deletions resources/js/data-access/memo-api/tquery/filter_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export function invert(filter: FilterH, invert?: boolean): FilterH {
}
}

function otherBoolOp(op: "&" | "|") {
return op === "&" ? "|" : "&";
}

export class FilterReductor {
private readonly columnsData = new Map<ColumnName, ColumnSchema>();

Expand Down Expand Up @@ -78,6 +82,9 @@ export class FilterReductor {
if (subFilter.type === "op" && subFilter.op === op && !subFilter.inv) {
// Unnest a bool operation of the same type.
subFiltersToProcess.push(...subFilter.val.toReversed());
} else if (subFilter.type === "op" && subFilter.op === otherBoolOp(op) && subFilter.inv) {
// Invert the inverted nested operation of the other type using De Morgan's laws.
subFiltersToProcess.push(...subFilter.val.map((f) => invert(f)).toReversed());
} else {
reducedSubFilters.push(subFilter);
}
Expand Down Expand Up @@ -111,7 +118,7 @@ export class FilterReductor {
if (val === "") {
// Frontend treats the null values in string columns as empty strings (because this is what
// is reasonable for the user).
if (op === "=" || op === "==" || op === "lv" || op === "<" || op === "<=") {
if (op === "=" || op === "==" || op === "lv" || op === "<=") {
// Matches only null strings.
return nullFilter();
} else if (op === ">") {
Expand All @@ -120,7 +127,7 @@ export class FilterReductor {
} else if (op === ">=" || op === "v%" || op === "%v" || op === "%v%" || op === "/v/") {
// Matches everything.
return "always";
} else if (op === "has") {
} else if (op === "<" || op === "has") {
// Even a null column is not considered to "have an empty string", but rather to contain nothing.
return "never";
} else if (op === "in" || op === "has_all" || op === "has_any" || op === "has_only") {
Expand Down

0 comments on commit 23b4166

Please sign in to comment.