Skip to content
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Rollback: Drop trigger and function
DROP TRIGGER IF EXISTS maintain_unique_test_names_trigger ON tests;
DROP FUNCTION IF EXISTS maintain_unique_test_names();

-- Drop table
DROP TABLE IF EXISTS unique_test_names;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- Create unique_test_names table to optimize autocomplete searches
CREATE TABLE IF NOT EXISTS unique_test_names (
name TEXT NOT NULL PRIMARY KEY
);

-- Create GIN index on name for LIKE '%pattern%' searches
CREATE INDEX IF NOT EXISTS unique_test_names_trgm_idx ON unique_test_names USING GIN (name gin_trgm_ops);

-- Seed the table with existing test names from tests table
INSERT INTO unique_test_names (name)
SELECT DISTINCT name FROM tests
ON CONFLICT (name) DO NOTHING;

-- Create trigger function to maintain unique_test_names when tests table changes
CREATE OR REPLACE FUNCTION maintain_unique_test_names()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
-- Insert new test name if it doesn't exist
INSERT INTO unique_test_names (name)
VALUES (NEW.name)
ON CONFLICT (name) DO NOTHING;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
-- Remove test name only if no other tests have this name
DELETE FROM unique_test_names
WHERE name = OLD.name
AND NOT EXISTS (SELECT 1 FROM tests WHERE name = OLD.name);
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

-- Create trigger on tests table to maintain unique_test_names
DROP TRIGGER IF EXISTS maintain_unique_test_names_trigger ON tests;
CREATE TRIGGER maintain_unique_test_names_trigger
AFTER INSERT OR UPDATE OR DELETE ON tests
FOR EACH ROW
EXECUTE FUNCTION maintain_unique_test_names();
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Rollback: Recreate trigram index on tests.name
CREATE INDEX IF NOT EXISTS tests_name_trgm_idx ON tests USING GIN (name gin_trgm_ops);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Drop trigram index on tests.name since we now search on unique_test_names table
-- Keep the regular btree index (tests_name_idx) for joins and equality filters
DROP INDEX IF EXISTS tests_name_trgm_idx;
91 changes: 88 additions & 3 deletions blade/db/postgres/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,14 +685,13 @@ impl state::DB for Postgres {
}

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

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

let results = tests
let results = unique_test_names
.select(name)
.filter(name.like(format!("%{pattern}%")))
.distinct()
.order_by(name.asc())
.limit(limit_i64)
.load::<String>(&mut self.conn)
Expand Down Expand Up @@ -1280,4 +1279,90 @@ mod tests {
assert_eq!(history.history.len(), 1);
assert_eq!(history.history[0].invocation_id, "inv1");
}

#[test]
fn test_search_test_names_trigger() {
use std::time::{Duration, SystemTime};

let tmp = tempdir::TempDir::new("test_search_test_names").unwrap();
let harness = harness::new(tmp.path().to_str().unwrap()).unwrap();
let uri = harness.uri();
super::init_db(&uri).unwrap();
let mgr = crate::manager::PostgresManager::new(&uri).unwrap();
let mut db = mgr.get().unwrap();

// Create invocation
let inv = state::InvocationResults {
id: "inv1".to_string(),
command: "test".to_string(),
status: state::Status::Success,
start: SystemTime::now(),
is_live: false,
..Default::default()
};
db.upsert_shallow_invocation(&inv).unwrap();

// Test 1: Insert first test name
let test1 = state::Test {
name: "//path/to/test:one".to_string(),
status: state::Status::Success,
duration: Duration::from_secs(1),
end: SystemTime::now(),
num_runs: 1,
runs: vec![],
};
db.upsert_test("inv1", &test1).unwrap();

let results = db.search_test_names("test:one", 10).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], "//path/to/test:one");

// Test 2: Insert second test name
let test2 = state::Test {
name: "//path/to/test:two".to_string(),
status: state::Status::Success,
duration: Duration::from_secs(2),
end: SystemTime::now(),
num_runs: 1,
runs: vec![],
};
db.upsert_test("inv1", &test2).unwrap();

let results = db.search_test_names("test:", 10).unwrap();
assert_eq!(results.len(), 2);
assert!(results.contains(&"//path/to/test:one".to_string()));
assert!(results.contains(&"//path/to/test:two".to_string()));

// Test 3: Update existing test (should not create duplicate in
// unique_test_names)
let test1_updated = state::Test {
name: "//path/to/test:one".to_string(),
status: state::Status::Fail,
duration: Duration::from_secs(5),
end: SystemTime::now(),
num_runs: 2,
runs: vec![],
};
db.upsert_test("inv1", &test1_updated).unwrap();

let results = db.search_test_names("test:one", 10).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], "//path/to/test:one");

// Test 4: Search with different patterns
let results = db.search_test_names("two", 10).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], "//path/to/test:two");

let results = db.search_test_names("path", 10).unwrap();
assert_eq!(results.len(), 2);

// Test 5: Search with limit
let results = db.search_test_names("test:", 1).unwrap();
assert_eq!(results.len(), 1);

// Test 6: Search with no matches
let results = db.search_test_names("nonexistent", 10).unwrap();
assert_eq!(results.len(), 0);
}
}
7 changes: 7 additions & 0 deletions blade/db/postgres/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ diesel::table! {
}
}

diesel::table! {
unique_test_names (name) {
name -> Text,
}
}

diesel::joinable!(options -> invocations (invocation_id));
diesel::joinable!(targets -> invocations (invocation_id));
diesel::joinable!(testartifacts -> invocations (invocation_id));
Expand All @@ -95,4 +101,5 @@ diesel::allow_tables_to_appear_in_same_query!(
testartifacts,
testruns,
tests,
unique_test_names,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Rollback: Drop triggers and table
DROP TRIGGER IF EXISTS maintain_unique_test_names_insert;
DROP TRIGGER IF EXISTS maintain_unique_test_names_update;
DROP TRIGGER IF EXISTS maintain_unique_test_names_delete;

-- Drop table
DROP TABLE IF EXISTS unique_test_names;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- Create unique_test_names table to optimize autocomplete searches
CREATE TABLE IF NOT EXISTS unique_test_names (
name TEXT NOT NULL PRIMARY KEY
);

-- Create index on name for LIKE '%pattern%' searches
CREATE INDEX IF NOT EXISTS unique_test_names_idx ON unique_test_names (name);

-- Seed the table with existing test names from tests table
INSERT OR IGNORE INTO unique_test_names (name)
SELECT DISTINCT name FROM tests;

-- Create trigger function to maintain unique_test_names when tests table changes
-- SQLite uses INSTEAD OF triggers for insert/update/delete, but since we're triggering AFTER,
-- we need separate triggers for each operation.

-- Trigger on INSERT: add new test name if it doesn't exist
CREATE TRIGGER IF NOT EXISTS maintain_unique_test_names_insert
AFTER INSERT ON tests
FOR EACH ROW
WHEN NEW.name NOT IN (SELECT name FROM unique_test_names)
BEGIN
INSERT INTO unique_test_names (name)
VALUES (NEW.name);
END;

-- Trigger on UPDATE: add updated test name if it doesn't exist
CREATE TRIGGER IF NOT EXISTS maintain_unique_test_names_update
AFTER UPDATE ON tests
FOR EACH ROW
WHEN NEW.name NOT IN (SELECT name FROM unique_test_names)
BEGIN
INSERT INTO unique_test_names (name)
VALUES (NEW.name);
END;

-- Trigger on DELETE: remove test name only if no other tests have this name
CREATE TRIGGER IF NOT EXISTS maintain_unique_test_names_delete
AFTER DELETE ON tests
FOR EACH ROW
BEGIN
DELETE FROM unique_test_names
WHERE name = OLD.name
AND NOT EXISTS (SELECT 1 FROM tests WHERE name = OLD.name);
END;
88 changes: 85 additions & 3 deletions blade/db/sqlite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,14 +708,13 @@ impl state::DB for Sqlite {
}

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

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

let results = Tests
let results = unique_test_names
.select(name)
.filter(name.like(format!("%{pattern}%")))
.distinct()
.order_by(name.asc())
.limit(limit_i64)
.load::<String>(&mut self.conn)
Expand Down Expand Up @@ -1341,4 +1340,87 @@ mod tests {
assert_eq!(history.history.len(), 1);
assert_eq!(history.history[0].invocation_id, "inv1");
}

#[test]
fn test_search_test_names_trigger() {
let tmp = tempdir::TempDir::new("test_search_test_names").unwrap();
let db_path = tmp.path().join("test.db");
super::init_db(db_path.to_str().unwrap()).unwrap();
let mgr = crate::manager::SqliteManager::new(db_path.to_str().unwrap()).unwrap();
let mut db = mgr.get().unwrap();

// Create invocation
let inv = state::InvocationResults {
id: "inv1".to_string(),
command: "test".to_string(),
status: state::Status::Success,
start: std::time::SystemTime::now(),
is_live: false,
..Default::default()
};
db.upsert_shallow_invocation(&inv).unwrap();

// Test 1: Insert first test name
let test1 = state::Test {
name: "//path/to/test:one".to_string(),
status: state::Status::Success,
duration: std::time::Duration::from_secs(1),
end: std::time::SystemTime::now(),
num_runs: 1,
runs: vec![],
};
db.upsert_test("inv1", &test1).unwrap();

let results = db.search_test_names("test:one", 10).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], "//path/to/test:one");

// Test 2: Insert second test name
let test2 = state::Test {
name: "//path/to/test:two".to_string(),
status: state::Status::Success,
duration: std::time::Duration::from_secs(2),
end: std::time::SystemTime::now(),
num_runs: 1,
runs: vec![],
};
db.upsert_test("inv1", &test2).unwrap();

let results = db.search_test_names("test:", 10).unwrap();
assert_eq!(results.len(), 2);
assert!(results.contains(&"//path/to/test:one".to_string()));
assert!(results.contains(&"//path/to/test:two".to_string()));

// Test 3: Update existing test (should not create duplicate in
// unique_test_names)
let test1_updated = state::Test {
name: "//path/to/test:one".to_string(),
status: state::Status::Fail,
duration: std::time::Duration::from_secs(5),
end: std::time::SystemTime::now(),
num_runs: 2,
runs: vec![],
};
db.upsert_test("inv1", &test1_updated).unwrap();

let results = db.search_test_names("test:one", 10).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], "//path/to/test:one");

// Test 4: Search with different patterns
let results = db.search_test_names("two", 10).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], "//path/to/test:two");

let results = db.search_test_names("path", 10).unwrap();
assert_eq!(results.len(), 2);

// Test 5: Search with limit
let results = db.search_test_names("test:", 1).unwrap();
assert_eq!(results.len(), 1);

// Test 6: Search with no matches
let results = db.search_test_names("nonexistent", 10).unwrap();
assert_eq!(results.len(), 0);
}
}
7 changes: 7 additions & 0 deletions blade/db/sqlite/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ diesel::table! {
}
}

diesel::table! {
unique_test_names (name) {
name -> Text,
}
}

diesel::joinable!(Options -> Invocations (invocation_id));
diesel::joinable!(Targets -> Invocations (invocation_id));
diesel::joinable!(TestArtifacts -> Invocations (invocation_id));
Expand All @@ -94,4 +100,5 @@ diesel::allow_tables_to_appear_in_same_query!(
TestArtifacts,
TestRuns,
Tests,
unique_test_names,
);