refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- Move the Tauri app (React frontend + Rust backend) into desktop/ via git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts) - Root package.json becomes a private bun workspace container with orchestration scripts; desktop package renamed gridline-desktop - Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin, static output, ready for Dokploy) - Update release.yml for the new layout: projectPath: desktop, desktop/src-tauri resource paths, rust-cache workspace path - Update README + AGENTS.md structure trees and dev commands - Verified: vitest (1074), cargo test (354), desktop build, tauri dev (window launches), tauri build (dmg + app bundles), Astro build
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
use rusqlite::Connection;
|
||||
|
||||
/// All new-column additions for the connections table since version 1.
|
||||
const CONNECTION_COLUMNS_V2: &[(&str, &str)] = &[
|
||||
("database", "TEXT"),
|
||||
("ssh_host", "TEXT"),
|
||||
("ssh_port", "INTEGER"),
|
||||
("ssh_user", "TEXT"),
|
||||
("ssh_auth_method", "TEXT"),
|
||||
("ssh_private_key_path", "TEXT"),
|
||||
("ssl_mode", "TEXT"),
|
||||
("ssl_ca_path", "TEXT"),
|
||||
("ssl_cert_path", "TEXT"),
|
||||
("ssl_key_path", "TEXT"),
|
||||
];
|
||||
|
||||
/// New columns added since version 2.
|
||||
const CONNECTION_COLUMNS_V3: &[(&str, &str)] = &[("environment", "TEXT")];
|
||||
|
||||
/// New columns added in version 7.
|
||||
const CONNECTION_COLUMNS_V7: &[(&str, &str)] = &[("favorite", "INTEGER NOT NULL DEFAULT 0")];
|
||||
|
||||
pub fn run_migrations(conn: &Connection) -> Result<(), String> {
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);
|
||||
CREATE TABLE IF NOT EXISTS folders (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
parent_id TEXT REFERENCES folders(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
db_type TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER,
|
||||
username TEXT,
|
||||
database TEXT,
|
||||
folder_id TEXT REFERENCES folders(id) ON DELETE SET NULL,
|
||||
keychain_ref TEXT,
|
||||
ssh_host TEXT,
|
||||
ssh_port INTEGER,
|
||||
ssh_user TEXT,
|
||||
ssh_auth_method TEXT,
|
||||
ssh_private_key_path TEXT,
|
||||
ssl_mode TEXT,
|
||||
ssl_ca_path TEXT,
|
||||
ssl_cert_path TEXT,
|
||||
ssl_key_path TEXT,
|
||||
environment TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
color TEXT NOT NULL DEFAULT '#8b5cf6',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS connection_tags (
|
||||
connection_id TEXT NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
|
||||
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (connection_id, tag_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS folder_tags (
|
||||
folder_id TEXT NOT NULL REFERENCES folders(id) ON DELETE CASCADE,
|
||||
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (folder_id, tag_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// --- Version-specific migrations ----------------------------------------
|
||||
|
||||
let current_ver: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(version), 0) FROM schema_version",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap_or(0);
|
||||
|
||||
if current_ver < 2 {
|
||||
// Discover which columns the connections table already has.
|
||||
let existing: Vec<String> = {
|
||||
let mut stmt = conn
|
||||
.prepare("PRAGMA table_info(connections)")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| row.get::<_, String>(1))
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
};
|
||||
|
||||
for (col_name, col_type) in CONNECTION_COLUMNS_V2 {
|
||||
if !existing.contains(&col_name.to_string()) {
|
||||
let sql = format!(
|
||||
"ALTER TABLE connections ADD COLUMN {} {}",
|
||||
col_name, col_type
|
||||
);
|
||||
conn.execute(&sql, []).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Record the migration.
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (2)", [])
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
if current_ver < 3 {
|
||||
let existing: Vec<String> = {
|
||||
let mut stmt = conn
|
||||
.prepare("PRAGMA table_info(connections)")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| row.get::<_, String>(1))
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
};
|
||||
|
||||
for (col_name, col_type) in CONNECTION_COLUMNS_V3 {
|
||||
if !existing.contains(&col_name.to_string()) {
|
||||
let sql = format!(
|
||||
"ALTER TABLE connections ADD COLUMN {} {}",
|
||||
col_name, col_type
|
||||
);
|
||||
conn.execute(&sql, []).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (3)", [])
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
// v4: backup_history
|
||||
if current_ver < 4 {
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS backup_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
connection_id TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('dump', 'restore', 'sync')),
|
||||
format TEXT,
|
||||
file_path TEXT,
|
||||
source_connection_id TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'running'
|
||||
CHECK(status IN ('running', 'completed', 'failed', 'cancelled')),
|
||||
error_message TEXT,
|
||||
size_bytes INTEGER,
|
||||
started_at TEXT NOT NULL,
|
||||
completed_at TEXT
|
||||
);",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (4)", [])
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
// v5: query_history
|
||||
if current_ver < 5 {
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS query_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
connection_id TEXT NOT NULL,
|
||||
query_text TEXT NOT NULL,
|
||||
execution_time_ms INTEGER,
|
||||
row_count INTEGER,
|
||||
status TEXT NOT NULL CHECK(status IN ('success', 'error')),
|
||||
error_message TEXT,
|
||||
executed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_query_history_connection
|
||||
ON query_history(connection_id, executed_at DESC);",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (5)", [])
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
// v6: query history favorites + saved queries
|
||||
if current_ver < 6 {
|
||||
conn.execute_batch(
|
||||
"ALTER TABLE query_history ADD COLUMN favorite INTEGER NOT NULL DEFAULT 0;
|
||||
CREATE TABLE IF NOT EXISTS queries (
|
||||
id TEXT PRIMARY KEY,
|
||||
connection_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
query_text TEXT NOT NULL,
|
||||
folder TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_queries_connection ON queries(connection_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_queries_folder ON queries(folder);",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (6)", [])
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
// v7: connection favorites + recent_connections
|
||||
if current_ver < 7 {
|
||||
let existing: Vec<String> = {
|
||||
let mut stmt = conn
|
||||
.prepare("PRAGMA table_info(connections)")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| row.get::<_, String>(1))
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
};
|
||||
|
||||
for (col_name, col_type) in CONNECTION_COLUMNS_V7 {
|
||||
if !existing.contains(&col_name.to_string()) {
|
||||
let sql = format!(
|
||||
"ALTER TABLE connections ADD COLUMN {} {}",
|
||||
col_name, col_type
|
||||
);
|
||||
conn.execute(&sql, []).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS recent_connections (
|
||||
connection_id TEXT PRIMARY KEY REFERENCES connections(id) ON DELETE CASCADE,
|
||||
opened_at TEXT NOT NULL
|
||||
);",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (7)", [])
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
// v8: use_keychain opt-out flag on connections
|
||||
if current_ver < 8 {
|
||||
let existing: Vec<String> = {
|
||||
let mut stmt = conn
|
||||
.prepare("PRAGMA table_info(connections)")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| row.get::<_, String>(1))
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
};
|
||||
|
||||
if !existing.iter().any(|c| c == "use_keychain") {
|
||||
conn.execute(
|
||||
"ALTER TABLE connections ADD COLUMN use_keychain INTEGER NOT NULL DEFAULT 1",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (8)", [])
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rusqlite::Connection;
|
||||
|
||||
fn fresh_db() -> Connection {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
conn
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrations_create_all_tables() {
|
||||
let conn = fresh_db();
|
||||
let tables: Vec<String> = conn
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
.unwrap()
|
||||
.query_map([], |row| row.get(0))
|
||||
.unwrap()
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
assert!(tables.contains(&"folders".to_string()));
|
||||
assert!(tables.contains(&"connections".to_string()));
|
||||
assert!(tables.contains(&"tags".to_string()));
|
||||
assert!(tables.contains(&"connection_tags".to_string()));
|
||||
assert!(tables.contains(&"settings".to_string()));
|
||||
assert!(tables.contains(&"schema_version".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v4_creates_backup_history_table() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM backup_history", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrations_are_idempotent() {
|
||||
let conn = fresh_db();
|
||||
// Running again must not error
|
||||
run_migrations(&conn).unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM schema_version", [], |row| row.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(count, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_v8_adds_use_keychain_column_default_1() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
// v7 baseline
|
||||
run_migrations(&conn).unwrap();
|
||||
// simulate an existing connection row (pre-v8 shape had no use_keychain)
|
||||
conn.execute(
|
||||
"INSERT INTO connections (id, name, db_type, host, port, username, database, folder_id, keychain_ref, ssh_host, ssh_port, ssh_user, ssh_auth_method, ssh_private_key_path, ssl_mode, ssl_ca_path, ssl_cert_path, ssl_key_path, environment, created_at, updated_at) VALUES ('c1','n','postgresql','h',5432,'u','d',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'t','t','t')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
let uses: i64 = conn
|
||||
.query_row("SELECT use_keychain FROM connections WHERE id='c1'", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(uses, 1, "existing connections default to use_keychain=1 (ON)");
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM schema_version WHERE version=8", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(count, 1, "schema_version row 8 inserted exactly once");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_v8_is_idempotent() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
let v8: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM schema_version WHERE version=8", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(v8, 1);
|
||||
// PRAGMA confirms exactly one use_keychain column
|
||||
let cols: Vec<String> = conn
|
||||
.prepare("PRAGMA table_info(connections)")
|
||||
.unwrap()
|
||||
.query_map([], |r| r.get::<_, String>(1))
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
assert_eq!(cols.iter().filter(|c| c == &"use_keychain").count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v5_creates_query_history_table() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
// Verify the table exists
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM query_history", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
// Verify columns via PRAGMA
|
||||
let columns: Vec<String> = {
|
||||
let mut stmt = conn.prepare("PRAGMA table_info(query_history)").unwrap();
|
||||
let rows = stmt.query_map([], |row| row.get::<_, String>(1)).unwrap();
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
};
|
||||
assert!(columns.contains(&"id".to_string()));
|
||||
assert!(columns.contains(&"connection_id".to_string()));
|
||||
assert!(columns.contains(&"query_text".to_string()));
|
||||
assert!(columns.contains(&"execution_time_ms".to_string()));
|
||||
assert!(columns.contains(&"row_count".to_string()));
|
||||
assert!(columns.contains(&"status".to_string()));
|
||||
assert!(columns.contains(&"error_message".to_string()));
|
||||
assert!(columns.contains(&"executed_at".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_history_cascades_on_connection_delete() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
// Insert a connection
|
||||
let conn_id = "test-conn-id";
|
||||
conn.execute(
|
||||
"INSERT INTO connections (id, name, db_type, host, port, created_at, updated_at) VALUES (?1, 't', 'postgresql', 'h', 5432, datetime('now'), datetime('now'))",
|
||||
rusqlite::params![conn_id],
|
||||
).unwrap();
|
||||
// Insert query history entry
|
||||
conn.execute(
|
||||
"INSERT INTO query_history (id, connection_id, query_text, status, executed_at) VALUES ('qh1', ?1, 'SELECT 1', 'success', datetime('now'))",
|
||||
rusqlite::params![conn_id],
|
||||
).unwrap();
|
||||
// Delete connection — should cascade
|
||||
conn.execute(
|
||||
"DELETE FROM connections WHERE id = ?1",
|
||||
rusqlite::params![conn_id],
|
||||
)
|
||||
.unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM query_history WHERE connection_id = ?1",
|
||||
rusqlite::params![conn_id],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v6_adds_favorite_column_to_query_history() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
// Verify the favorite column exists via PRAGMA
|
||||
let columns: Vec<String> = {
|
||||
let mut stmt = conn.prepare("PRAGMA table_info(query_history)").unwrap();
|
||||
let rows = stmt.query_map([], |row| row.get::<_, String>(1)).unwrap();
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
};
|
||||
assert!(
|
||||
columns.contains(&"favorite".to_string()),
|
||||
"Expected query_history to have a 'favorite' column after v6 migration"
|
||||
);
|
||||
// Existing rows default to 0
|
||||
let fav: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM query_history WHERE favorite != 0",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fav, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v6_creates_queries_table() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
// Table exists
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM queries", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
// Column check
|
||||
let columns: Vec<String> = {
|
||||
let mut stmt = conn.prepare("PRAGMA table_info(queries)").unwrap();
|
||||
let rows = stmt.query_map([], |row| row.get::<_, String>(1)).unwrap();
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
};
|
||||
for c in &[
|
||||
"id",
|
||||
"connection_id",
|
||||
"name",
|
||||
"query_text",
|
||||
"folder",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
] {
|
||||
assert!(
|
||||
columns.contains(&c.to_string()),
|
||||
"Expected queries table to have column: {}",
|
||||
c
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v6_queries_cascade_on_connection_delete() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
// Insert a connection
|
||||
let conn_id = "test-qc-conn";
|
||||
conn.execute(
|
||||
"INSERT INTO connections (id, name, db_type, host, port, created_at, updated_at) VALUES (?1, 't', 'postgresql', 'h', 5432, datetime('now'), datetime('now'))",
|
||||
rusqlite::params![conn_id],
|
||||
).unwrap();
|
||||
// Insert a saved query for that connection
|
||||
conn.execute(
|
||||
"INSERT INTO queries (id, connection_id, name, query_text, folder) VALUES ('q1', ?1, 'my query', 'SELECT 1', '')",
|
||||
rusqlite::params![conn_id],
|
||||
).unwrap();
|
||||
// Insert a global saved query (connection_id NULL)
|
||||
conn.execute(
|
||||
"INSERT INTO queries (id, connection_id, name, query_text, folder) VALUES ('q2', NULL, 'global query', 'SELECT 2', '')",
|
||||
[],
|
||||
).unwrap();
|
||||
// Delete connection — should cascade the non-NULL row
|
||||
conn.execute(
|
||||
"DELETE FROM connections WHERE id = ?1",
|
||||
rusqlite::params![conn_id],
|
||||
)
|
||||
.unwrap();
|
||||
let count_scoped: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM queries WHERE connection_id = ?1",
|
||||
rusqlite::params![conn_id],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
count_scoped, 0,
|
||||
"Scoped saved query should be cascade-deleted"
|
||||
);
|
||||
// NULL-saved query survives
|
||||
let count_global: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM queries WHERE id = 'q2'", [], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
count_global, 1,
|
||||
"Global saved query (connection_id NULL) should survive"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v6_bumps_schema_version_to_6() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM schema_version WHERE version = 6",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
count, 1,
|
||||
"Schema version 6 should be recorded after v6 migration"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v7_adds_favorite_column_to_connections() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
let columns: Vec<String> = {
|
||||
let mut stmt = conn.prepare("PRAGMA table_info(connections)").unwrap();
|
||||
let rows = stmt.query_map([], |row| row.get::<_, String>(1)).unwrap();
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
};
|
||||
assert!(
|
||||
columns.contains(&"favorite".to_string()),
|
||||
"Expected connections to have a 'favorite' column after v7"
|
||||
);
|
||||
let conn_id = "fav-test";
|
||||
conn.execute(
|
||||
"INSERT INTO connections (id, name, db_type, host, port, created_at, updated_at) VALUES (?1, 't', 'postgresql', 'h', 5432, datetime('now'), datetime('now'))",
|
||||
rusqlite::params![conn_id],
|
||||
).unwrap();
|
||||
let fav: i64 = conn
|
||||
.query_row(
|
||||
"SELECT favorite FROM connections WHERE id = ?1",
|
||||
rusqlite::params![conn_id],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fav, 0, "favorite defaults to 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v7_creates_recent_connections_table() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM recent_connections", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v7_recent_connections_cascade_on_connection_delete() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
let conn_id = "rc-cascade";
|
||||
conn.execute(
|
||||
"INSERT INTO connections (id, name, db_type, host, port, created_at, updated_at) VALUES (?1, 't', 'postgresql', 'h', 5432, datetime('now'), datetime('now'))",
|
||||
rusqlite::params![conn_id],
|
||||
).unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO recent_connections (connection_id, opened_at) VALUES (?1, datetime('now'))",
|
||||
rusqlite::params![conn_id],
|
||||
).unwrap();
|
||||
conn.execute(
|
||||
"DELETE FROM connections WHERE id = ?1",
|
||||
rusqlite::params![conn_id],
|
||||
)
|
||||
.unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM recent_connections WHERE connection_id = ?1",
|
||||
rusqlite::params![conn_id],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v8_bumps_schema_version_to_8() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
let ver: i64 = conn
|
||||
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(ver, 8);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user