Revert "Add analytics table search bar routing and validation logic."

This reverts commit 78af13b5dc.
This commit is contained in:
Developing-Gamer
2026-07-14 11:14:28 -07:00
parent 32f0cfad56
commit 5c7c202fb2
2 changed files with 0 additions and 214 deletions
@@ -1,112 +0,0 @@
import { describe, expect, it } from "vitest";
import {
getValidatedTableFilterQuery,
looksLikeNaturalLanguageQuery,
} from "./search-bar-logic";
describe("looksLikeNaturalLanguageQuery", () => {
it.each([
// plain substring searches stay plain
["", false],
[" ", false],
["alice", false],
["[email protected]", false],
["[email protected]", false], // single token; "no" cue must not trigger
["4c5ba425-6c22-4f65-9e33-3e2b1c5a8d1f", false],
["/dashboard/settings", false],
["utm_source=google", false], // bare "=" appears in real data values
["john smith", false],
["page view", false],
// natural-language conditions go to the AI
["users who signed up last week", true],
["verified users", true],
["more than 5 events", true],
["signed up before january", true],
["emails that bounced", true],
["count > 10", true],
["how many users are there?", true],
["anything at all?", true], // trailing question mark is always a question
["users without a display name", true],
["events from the past 3 days", true],
])("%j → %s", (input, expected) => {
expect(looksLikeNaturalLanguageQuery(input)).toBe(expected);
});
});
describe("getValidatedTableFilterQuery", () => {
it("accepts a bare SELECT * on the table", () => {
expect(getValidatedTableFilterQuery("SELECT * FROM users", "users")).toBe(
"SELECT * FROM users",
);
});
it("accepts WHERE filters, default. prefixes, backticks, and mixed case", () => {
expect(
getValidatedTableFilterQuery(
"select * from default.users where primary_email_verified = 1",
"users",
),
).toBe("select * from default.users where primary_email_verified = 1");
expect(
getValidatedTableFilterQuery(
"SELECT * FROM `users` WHERE signed_up_at >= now() - INTERVAL 7 DAY",
"users",
),
).toContain("WHERE signed_up_at");
});
it("accepts multi-line queries and strips trailing semicolons", () => {
expect(
getValidatedTableFilterQuery(
"SELECT *\nFROM users\nWHERE is_anonymous = 0;",
"users",
),
).toBe("SELECT *\nFROM users\nWHERE is_anonymous = 0");
});
it("accepts subqueries inside the WHERE condition", () => {
const query =
"SELECT * FROM users WHERE toString(id) IN (SELECT user_id FROM events GROUP BY user_id HAVING count() > 5 LIMIT 100)";
expect(getValidatedTableFilterQuery(query, "users")).toBe(query);
});
it("rejects queries on a different table", () => {
expect(getValidatedTableFilterQuery("SELECT * FROM events", "users")).toBe(null);
// prefix of another table name must not match
expect(getValidatedTableFilterQuery("SELECT * FROM users2 WHERE 1", "users")).toBe(null);
expect(getValidatedTableFilterQuery("SELECT * FROM users_archive", "users")).toBe(null);
});
it("rejects anything that could change the column set", () => {
expect(
getValidatedTableFilterQuery("SELECT id, primary_email FROM users", "users"),
).toBe(null);
expect(
getValidatedTableFilterQuery("SELECT count() FROM users", "users"),
).toBe(null);
expect(
getValidatedTableFilterQuery(
"SELECT * FROM users u JOIN events e ON toString(u.id) = e.user_id",
"users",
),
).toBe(null);
expect(
getValidatedTableFilterQuery(
"SELECT * FROM users, events",
"users",
),
).toBe(null);
expect(
getValidatedTableFilterQuery(
"SELECT * FROM users GROUP BY id",
"users",
),
).toBe(null);
});
it("rejects empty and non-SELECT input", () => {
expect(getValidatedTableFilterQuery("", "users")).toBe(null);
expect(getValidatedTableFilterQuery(";", "users")).toBe(null);
expect(getValidatedTableFilterQuery("DROP TABLE users", "users")).toBe(null);
});
});
@@ -1,102 +0,0 @@
/**
* Pure decision logic for the analytics table search bar, kept free of React
* so the heuristics can be unit-tested exhaustively (see
* search-bar-logic.test.ts).
*
* The search bar is filter-first: typing applies a plain substring (ILIKE)
* filter over the current table. Only input that a substring match cannot
* express is routed to the AI, which must respond with a row filter of the
* shape `SELECT * FROM <table> WHERE ...` so the grid's columns never change.
*/
/**
* Words that signal the user is describing a CONDITION ("users who signed up
* last week") rather than typing a substring to match ("[email protected]").
* Deliberately excludes anything likely to appear inside real data values —
* e.g. bare "com" or "www" — since false positives would hijack ordinary
* searches.
*/
const NATURAL_LANGUAGE_CUES = new Set([
// question / command words
"who", "whose", "which", "whom", "what", "when", "how", "why",
"show", "find", "list", "give", "display", "filter", "get",
// comparison / quantity words
"more", "less", "most", "least", "fewer", "greater", "than",
"under", "over", "above", "below", "between", "top", "bottom", "count",
// temporal words
"before", "after", "since", "ago", "during", "past", "last", "first",
"latest", "newest", "oldest", "recent", "recently", "earlier",
"today", "yesterday", "tomorrow",
"day", "days", "week", "weeks", "month", "months", "year", "years",
"hour", "hours", "minute", "minutes",
// state / negation words
"verified", "unverified", "anonymous", "empty", "missing", "null",
"never", "ever", "only", "all", "any", "every", "none", "not", "without",
// relational / auxiliary words common in questions
"with", "have", "has", "had", "are", "is", "was", "were", "that",
"contains", "containing", "starts", "ends", "starting", "ending",
// domain verbs that read as conditions
"signed", "created", "updated", "joined", "expired", "delivered",
"opened", "clicked", "bounced", "sent",
// plural domain nouns — people describe segments with these ("verified
// users"), while substring searches contain concrete values instead
"users", "teams", "members", "accounts", "sessions", "invitations",
"permissions", "channels", "tokens", "emails", "events",
]);
/**
* Heuristic: does this input look like a natural-language request (→ send to
* the AI on Enter) rather than a plain substring search (→ keep the live
* filter)? The zero-results fallback in the search bar catches misses, so
* this errs on the side of NOT flagging: single-token input (emails, IDs,
* paths) is never natural language.
*/
export function looksLikeNaturalLanguageQuery(text: string): boolean {
const trimmed = text.trim();
if (trimmed.length === 0) return false;
// Comparisons can never be expressed as a substring match. Bare `=` is
// deliberately NOT a cue — it shows up in legitimate substring searches
// like URL query params ("utm_source=google").
if (/[<>]=?|!=/.test(trimmed)) return true;
if (trimmed.endsWith("?")) return true;
// Single tokens (emails, UUIDs, paths) are always substring searches, even
// when their word-fragments happen to hit the cue list ("no-reply@…").
if (!/\s/.test(trimmed)) return false;
const words = trimmed.toLowerCase().match(/[a-z']+/g) ?? [];
const cueCount = words.reduce(
(count, word) => count + (NATURAL_LANGUAGE_CUES.has(word) ? 1 : 0),
0,
);
return (words.length >= 3 && cueCount >= 1) || cueCount >= 2;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Validates that an AI-committed query is a pure row filter over the given
* table, i.e. it cannot change the grid's columns. Accepts
* `SELECT * FROM [default.]<table>` optionally followed by
* WHERE/PREWHERE/ORDER BY/LIMIT — anything else (column lists, JOINs at the
* top level, other tables) returns null and must not be applied to the grid.
* Subqueries inside the WHERE condition are fine: the `SELECT *` prefix on
* the outer query is what guarantees the column set stays identical.
*
* Returns the query normalized for embedding as a subquery (trailing
* semicolons stripped — the grid wraps it in `SELECT * FROM (...)`).
*/
export function getValidatedTableFilterQuery(
query: string,
tableName: string,
): string | null {
const normalized = query.trim().replace(/;+\s*$/, "").trim();
if (normalized.length === 0) return null;
const collapsed = normalized.replace(/\s+/g, " ");
const table = escapeRegExp(tableName);
const pattern = new RegExp(
`^select \\* from (?:default\\.)?\`?${table}\`?(?: (?:where|prewhere|order by|limit)\\b.*)?$`,
"i",
);
return pattern.test(collapsed) ? normalized : null;
}