use rusqlite::{params, Connection, OptionalExtension};
use crate::core::errors::{Result, TgaError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinearSyncCursor {
pub last_synced_at: String,
pub last_run_at: String,
pub issues_synced: i64,
}
pub fn get_linear_cursor(conn: &Connection, team_key: &str) -> Result<Option<LinearSyncCursor>> {
conn.query_row(
"SELECT last_synced_at, last_run_at, issues_synced \
FROM linear_sync_cursor WHERE team_key = ?1",
params![team_key],
|row| {
Ok(LinearSyncCursor {
last_synced_at: row.get(0)?,
last_run_at: row.get(1)?,
issues_synced: row.get(2)?,
})
},
)
.optional()
.map_err(TgaError::from)
}
pub fn set_linear_cursor(
conn: &Connection,
team_key: &str,
last_synced_at: &str,
issues_synced: i64,
) -> Result<()> {
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT OR REPLACE INTO linear_sync_cursor \
(team_key, last_synced_at, last_run_at, issues_synced) \
VALUES (?1, ?2, ?3, ?4)",
params![team_key, last_synced_at, now, issues_synced],
)
.map_err(TgaError::from)?;
Ok(())
}
pub fn list_linear_cursor_teams(conn: &Connection) -> Result<Vec<String>> {
let mut stmt = conn
.prepare("SELECT team_key FROM linear_sync_cursor ORDER BY team_key")
.map_err(TgaError::from)?;
let rows = stmt
.query_map([], |row| row.get::<_, String>(0))
.map_err(TgaError::from)?;
let mut out = Vec::new();
for row in rows {
out.push(row.map_err(TgaError::from)?);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::db::Database;
#[test]
fn get_cursor_is_none_before_any_sync() {
let db = Database::open_in_memory().expect("open");
assert_eq!(
get_linear_cursor(db.connection(), "ENG").expect("query"),
None
);
}
#[test]
fn set_then_get_cursor_roundtrips() {
let db = Database::open_in_memory().expect("open");
set_linear_cursor(db.connection(), "ENG", "2026-01-01T00:00:00+00:00", 42).expect("set");
let cursor = get_linear_cursor(db.connection(), "ENG")
.expect("query")
.expect("present");
assert_eq!(cursor.last_synced_at, "2026-01-01T00:00:00+00:00");
assert_eq!(cursor.issues_synced, 42);
assert!(!cursor.last_run_at.is_empty());
}
#[test]
fn set_cursor_overwrites_the_prior_row() {
let db = Database::open_in_memory().expect("open");
set_linear_cursor(db.connection(), "ENG", "2026-01-01T00:00:00+00:00", 10).expect("first");
set_linear_cursor(db.connection(), "ENG", "2026-02-01T00:00:00+00:00", 20).expect("second");
let cursor = get_linear_cursor(db.connection(), "ENG")
.expect("query")
.expect("present");
assert_eq!(cursor.last_synced_at, "2026-02-01T00:00:00+00:00");
assert_eq!(cursor.issues_synced, 20);
}
#[test]
fn list_cursor_teams_returns_every_recorded_team_sorted() {
let db = Database::open_in_memory().expect("open");
set_linear_cursor(db.connection(), "FE", "2026-01-01T00:00:00+00:00", 1).expect("fe");
set_linear_cursor(db.connection(), "ENG", "2026-01-01T00:00:00+00:00", 1).expect("eng");
let teams = list_linear_cursor_teams(db.connection()).expect("list");
assert_eq!(teams, vec!["ENG".to_string(), "FE".to_string()]);
}
}