Skip to content

Commit 3f0a1b8

Browse files
committed
create a test name table for faster auto complete
the tests table is huge and using it directly for autocomplete was slow
1 parent bf008ff commit 3f0a1b8

8 files changed

Lines changed: 285 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Rollback: Drop trigger and function
2+
DROP TRIGGER IF EXISTS maintain_unique_test_names_trigger ON tests;
3+
DROP FUNCTION IF EXISTS maintain_unique_test_names();
4+
5+
-- Drop table
6+
DROP TABLE IF EXISTS unique_test_names;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
-- Create unique_test_names table to optimize autocomplete searches
2+
CREATE TABLE IF NOT EXISTS unique_test_names (
3+
name TEXT NOT NULL PRIMARY KEY
4+
);
5+
6+
-- Create GIN index on name for LIKE '%pattern%' searches
7+
CREATE INDEX IF NOT EXISTS unique_test_names_trgm_idx ON unique_test_names USING GIN (name gin_trgm_ops);
8+
9+
-- Seed the table with existing test names from tests table
10+
INSERT INTO unique_test_names (name)
11+
SELECT DISTINCT name FROM tests
12+
ON CONFLICT (name) DO NOTHING;
13+
14+
-- Create trigger function to maintain unique_test_names when tests table changes
15+
CREATE OR REPLACE FUNCTION maintain_unique_test_names()
16+
RETURNS TRIGGER AS $$
17+
BEGIN
18+
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
19+
-- Insert new test name if it doesn't exist
20+
INSERT INTO unique_test_names (name)
21+
VALUES (NEW.name)
22+
ON CONFLICT (name) DO NOTHING;
23+
RETURN NEW;
24+
ELSIF TG_OP = 'DELETE' THEN
25+
-- Remove test name only if no other tests have this name
26+
DELETE FROM unique_test_names
27+
WHERE name = OLD.name
28+
AND NOT EXISTS (SELECT 1 FROM tests WHERE name = OLD.name);
29+
RETURN OLD;
30+
END IF;
31+
RETURN NULL;
32+
END;
33+
$$ LANGUAGE plpgsql;
34+
35+
-- Create trigger on tests table to maintain unique_test_names
36+
DROP TRIGGER IF EXISTS maintain_unique_test_names_trigger ON tests;
37+
CREATE TRIGGER maintain_unique_test_names_trigger
38+
AFTER INSERT OR UPDATE OR DELETE ON tests
39+
FOR EACH ROW
40+
EXECUTE FUNCTION maintain_unique_test_names();

blade/db/postgres/mod.rs

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,14 +685,13 @@ impl state::DB for Postgres {
685685
}
686686

687687
fn search_test_names(&mut self, pattern: &str, limit: usize) -> anyhow::Result<Vec<String>> {
688-
use schema::tests::dsl::*;
688+
use schema::unique_test_names::dsl::*;
689689

690690
let limit_i64: i64 = limit.try_into().context("failed to convert limit to i64")?;
691691

692-
let results = tests
692+
let results = unique_test_names
693693
.select(name)
694694
.filter(name.like(format!("%{pattern}%")))
695-
.distinct()
696695
.order_by(name.asc())
697696
.limit(limit_i64)
698697
.load::<String>(&mut self.conn)
@@ -1280,4 +1279,90 @@ mod tests {
12801279
assert_eq!(history.history.len(), 1);
12811280
assert_eq!(history.history[0].invocation_id, "inv1");
12821281
}
1282+
1283+
#[test]
1284+
fn test_search_test_names_trigger() {
1285+
use std::time::{Duration, SystemTime};
1286+
1287+
let tmp = tempdir::TempDir::new("test_search_test_names").unwrap();
1288+
let harness = harness::new(tmp.path().to_str().unwrap()).unwrap();
1289+
let uri = harness.uri();
1290+
super::init_db(&uri).unwrap();
1291+
let mgr = crate::manager::PostgresManager::new(&uri).unwrap();
1292+
let mut db = mgr.get().unwrap();
1293+
1294+
// Create invocation
1295+
let inv = state::InvocationResults {
1296+
id: "inv1".to_string(),
1297+
command: "test".to_string(),
1298+
status: state::Status::Success,
1299+
start: SystemTime::now(),
1300+
is_live: false,
1301+
..Default::default()
1302+
};
1303+
db.upsert_shallow_invocation(&inv).unwrap();
1304+
1305+
// Test 1: Insert first test name
1306+
let test1 = state::Test {
1307+
name: "//path/to/test:one".to_string(),
1308+
status: state::Status::Success,
1309+
duration: Duration::from_secs(1),
1310+
end: SystemTime::now(),
1311+
num_runs: 1,
1312+
runs: vec![],
1313+
};
1314+
db.upsert_test("inv1", &test1).unwrap();
1315+
1316+
let results = db.search_test_names("test:one", 10).unwrap();
1317+
assert_eq!(results.len(), 1);
1318+
assert_eq!(results[0], "//path/to/test:one");
1319+
1320+
// Test 2: Insert second test name
1321+
let test2 = state::Test {
1322+
name: "//path/to/test:two".to_string(),
1323+
status: state::Status::Success,
1324+
duration: Duration::from_secs(2),
1325+
end: SystemTime::now(),
1326+
num_runs: 1,
1327+
runs: vec![],
1328+
};
1329+
db.upsert_test("inv1", &test2).unwrap();
1330+
1331+
let results = db.search_test_names("test:", 10).unwrap();
1332+
assert_eq!(results.len(), 2);
1333+
assert!(results.contains(&"//path/to/test:one".to_string()));
1334+
assert!(results.contains(&"//path/to/test:two".to_string()));
1335+
1336+
// Test 3: Update existing test (should not create duplicate in
1337+
// unique_test_names)
1338+
let test1_updated = state::Test {
1339+
name: "//path/to/test:one".to_string(),
1340+
status: state::Status::Fail,
1341+
duration: Duration::from_secs(5),
1342+
end: SystemTime::now(),
1343+
num_runs: 2,
1344+
runs: vec![],
1345+
};
1346+
db.upsert_test("inv1", &test1_updated).unwrap();
1347+
1348+
let results = db.search_test_names("test:one", 10).unwrap();
1349+
assert_eq!(results.len(), 1);
1350+
assert_eq!(results[0], "//path/to/test:one");
1351+
1352+
// Test 4: Search with different patterns
1353+
let results = db.search_test_names("two", 10).unwrap();
1354+
assert_eq!(results.len(), 1);
1355+
assert_eq!(results[0], "//path/to/test:two");
1356+
1357+
let results = db.search_test_names("path", 10).unwrap();
1358+
assert_eq!(results.len(), 2);
1359+
1360+
// Test 5: Search with limit
1361+
let results = db.search_test_names("test:", 1).unwrap();
1362+
assert_eq!(results.len(), 1);
1363+
1364+
// Test 6: Search with no matches
1365+
let results = db.search_test_names("nonexistent", 10).unwrap();
1366+
assert_eq!(results.len(), 0);
1367+
}
12831368
}

blade/db/postgres/schema.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ diesel::table! {
7878
}
7979
}
8080

81+
diesel::table! {
82+
unique_test_names (name) {
83+
name -> Text,
84+
}
85+
}
86+
8187
diesel::joinable!(options -> invocations (invocation_id));
8288
diesel::joinable!(targets -> invocations (invocation_id));
8389
diesel::joinable!(testartifacts -> invocations (invocation_id));
@@ -95,4 +101,5 @@ diesel::allow_tables_to_appear_in_same_query!(
95101
testartifacts,
96102
testruns,
97103
tests,
104+
unique_test_names,
98105
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Rollback: Drop triggers and table
2+
DROP TRIGGER IF EXISTS maintain_unique_test_names_insert;
3+
DROP TRIGGER IF EXISTS maintain_unique_test_names_update;
4+
DROP TRIGGER IF EXISTS maintain_unique_test_names_delete;
5+
6+
-- Drop table
7+
DROP TABLE IF EXISTS unique_test_names;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
-- Create unique_test_names table to optimize autocomplete searches
2+
CREATE TABLE IF NOT EXISTS unique_test_names (
3+
name TEXT NOT NULL PRIMARY KEY
4+
);
5+
6+
-- Create index on name for LIKE '%pattern%' searches
7+
CREATE INDEX IF NOT EXISTS unique_test_names_idx ON unique_test_names (name);
8+
9+
-- Seed the table with existing test names from tests table
10+
INSERT OR IGNORE INTO unique_test_names (name)
11+
SELECT DISTINCT name FROM tests;
12+
13+
-- Create trigger function to maintain unique_test_names when tests table changes
14+
-- SQLite uses INSTEAD OF triggers for insert/update/delete, but since we're triggering AFTER,
15+
-- we need separate triggers for each operation.
16+
17+
-- Trigger on INSERT: add new test name if it doesn't exist
18+
CREATE TRIGGER IF NOT EXISTS maintain_unique_test_names_insert
19+
AFTER INSERT ON tests
20+
FOR EACH ROW
21+
WHEN NEW.name NOT IN (SELECT name FROM unique_test_names)
22+
BEGIN
23+
INSERT INTO unique_test_names (name)
24+
VALUES (NEW.name);
25+
END;
26+
27+
-- Trigger on UPDATE: add updated test name if it doesn't exist
28+
CREATE TRIGGER IF NOT EXISTS maintain_unique_test_names_update
29+
AFTER UPDATE ON tests
30+
FOR EACH ROW
31+
WHEN NEW.name NOT IN (SELECT name FROM unique_test_names)
32+
BEGIN
33+
INSERT INTO unique_test_names (name)
34+
VALUES (NEW.name);
35+
END;
36+
37+
-- Trigger on DELETE: remove test name only if no other tests have this name
38+
CREATE TRIGGER IF NOT EXISTS maintain_unique_test_names_delete
39+
AFTER DELETE ON tests
40+
FOR EACH ROW
41+
BEGIN
42+
DELETE FROM unique_test_names
43+
WHERE name = OLD.name
44+
AND NOT EXISTS (SELECT 1 FROM tests WHERE name = OLD.name);
45+
END;

blade/db/sqlite/mod.rs

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -708,14 +708,13 @@ impl state::DB for Sqlite {
708708
}
709709

710710
fn search_test_names(&mut self, pattern: &str, limit: usize) -> anyhow::Result<Vec<String>> {
711-
use schema::Tests::dsl::*;
711+
use schema::unique_test_names::dsl::*;
712712

713713
let limit_i64: i64 = limit.try_into().context("failed to convert limit to i64")?;
714714

715-
let results = Tests
715+
let results = unique_test_names
716716
.select(name)
717717
.filter(name.like(format!("%{pattern}%")))
718-
.distinct()
719718
.order_by(name.asc())
720719
.limit(limit_i64)
721720
.load::<String>(&mut self.conn)
@@ -1341,4 +1340,87 @@ mod tests {
13411340
assert_eq!(history.history.len(), 1);
13421341
assert_eq!(history.history[0].invocation_id, "inv1");
13431342
}
1343+
1344+
#[test]
1345+
fn test_search_test_names_trigger() {
1346+
let tmp = tempdir::TempDir::new("test_search_test_names").unwrap();
1347+
let db_path = tmp.path().join("test.db");
1348+
super::init_db(db_path.to_str().unwrap()).unwrap();
1349+
let mgr = crate::manager::SqliteManager::new(db_path.to_str().unwrap()).unwrap();
1350+
let mut db = mgr.get().unwrap();
1351+
1352+
// Create invocation
1353+
let inv = state::InvocationResults {
1354+
id: "inv1".to_string(),
1355+
command: "test".to_string(),
1356+
status: state::Status::Success,
1357+
start: std::time::SystemTime::now(),
1358+
is_live: false,
1359+
..Default::default()
1360+
};
1361+
db.upsert_shallow_invocation(&inv).unwrap();
1362+
1363+
// Test 1: Insert first test name
1364+
let test1 = state::Test {
1365+
name: "//path/to/test:one".to_string(),
1366+
status: state::Status::Success,
1367+
duration: std::time::Duration::from_secs(1),
1368+
end: std::time::SystemTime::now(),
1369+
num_runs: 1,
1370+
runs: vec![],
1371+
};
1372+
db.upsert_test("inv1", &test1).unwrap();
1373+
1374+
let results = db.search_test_names("test:one", 10).unwrap();
1375+
assert_eq!(results.len(), 1);
1376+
assert_eq!(results[0], "//path/to/test:one");
1377+
1378+
// Test 2: Insert second test name
1379+
let test2 = state::Test {
1380+
name: "//path/to/test:two".to_string(),
1381+
status: state::Status::Success,
1382+
duration: std::time::Duration::from_secs(2),
1383+
end: std::time::SystemTime::now(),
1384+
num_runs: 1,
1385+
runs: vec![],
1386+
};
1387+
db.upsert_test("inv1", &test2).unwrap();
1388+
1389+
let results = db.search_test_names("test:", 10).unwrap();
1390+
assert_eq!(results.len(), 2);
1391+
assert!(results.contains(&"//path/to/test:one".to_string()));
1392+
assert!(results.contains(&"//path/to/test:two".to_string()));
1393+
1394+
// Test 3: Update existing test (should not create duplicate in
1395+
// unique_test_names)
1396+
let test1_updated = state::Test {
1397+
name: "//path/to/test:one".to_string(),
1398+
status: state::Status::Fail,
1399+
duration: std::time::Duration::from_secs(5),
1400+
end: std::time::SystemTime::now(),
1401+
num_runs: 2,
1402+
runs: vec![],
1403+
};
1404+
db.upsert_test("inv1", &test1_updated).unwrap();
1405+
1406+
let results = db.search_test_names("test:one", 10).unwrap();
1407+
assert_eq!(results.len(), 1);
1408+
assert_eq!(results[0], "//path/to/test:one");
1409+
1410+
// Test 4: Search with different patterns
1411+
let results = db.search_test_names("two", 10).unwrap();
1412+
assert_eq!(results.len(), 1);
1413+
assert_eq!(results[0], "//path/to/test:two");
1414+
1415+
let results = db.search_test_names("path", 10).unwrap();
1416+
assert_eq!(results.len(), 2);
1417+
1418+
// Test 5: Search with limit
1419+
let results = db.search_test_names("test:", 1).unwrap();
1420+
assert_eq!(results.len(), 1);
1421+
1422+
// Test 6: Search with no matches
1423+
let results = db.search_test_names("nonexistent", 10).unwrap();
1424+
assert_eq!(results.len(), 0);
1425+
}
13441426
}

blade/db/sqlite/schema.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ diesel::table! {
7878
}
7979
}
8080

81+
diesel::table! {
82+
unique_test_names (name) {
83+
name -> Text,
84+
}
85+
}
86+
8187
diesel::joinable!(Options -> Invocations (invocation_id));
8288
diesel::joinable!(Targets -> Invocations (invocation_id));
8389
diesel::joinable!(TestArtifacts -> Invocations (invocation_id));
@@ -94,4 +100,5 @@ diesel::allow_tables_to_appear_in_same_query!(
94100
TestArtifacts,
95101
TestRuns,
96102
Tests,
103+
unique_test_names,
97104
);

0 commit comments

Comments
 (0)