@ -39,28 +39,31 @@ fn fetch_tags_for_quote(
Ok ( tags )
}
/// Map rusqlite columns (id, text, author, source, date, created_at, updated_at)
/// Map rusqlite columns (id, text, author, source, date, hidden, created_at, updated_at)
/// plus a pre-fetched tags vec into a [`Quote`].
fn row_to_quote ( row : & rusqlite ::Row < ' _ > , tags : Vec < String > ) -> Result < Quote , rusqlite ::Error > {
let hidden_int : i64 = row . get ( 5 ) ? ;
Ok ( Quote {
id : row . get ( 0 ) ? ,
text : row . get ( 1 ) ? ,
author : row . get ( 2 ) ? ,
source : row . get ( 3 ) ? ,
date : row . get ( 4 ) ? ,
created_at : row . get ( 5 ) ? ,
updated_at : row . get ( 6 ) ? ,
hidden : hidden_int ! = 0 ,
created_at : row . get ( 6 ) ? ,
updated_at : row . get ( 7 ) ? ,
tags ,
} )
}
#[ async_trait::async_trait ]
impl QuoteRepository for NativeRepository {
/// Run the five DDL migration statements from [`super::migrations`].
/// Run all DDL migration statements from [`super::migrations`].
///
/// Covers `quotes`, `quote_tags`, tag index, author index, and
/// `admin_config`. Safe to call multiple times — all statements use
/// `IF NOT EXISTS`.
/// Covers `quotes`, `quote_tags`, tag index, author index, `admin_config`,
/// and the `hidden` column ALTER. Safe to call multiple times — CREATE
/// statements use `IF NOT EXISTS`, and the ALTER TABLE error (column
/// already exists) is intentionally ignored.
async fn run_migrations ( & self ) -> Result < ( ) , DbError > {
self . conn
. call ( | conn | {
@ -69,6 +72,9 @@ impl QuoteRepository for NativeRepository {
" { CREATE_QUOTES } ; { CREATE_QUOTE_TAGS } ; \
{ CREATE_TAG_INDEX } ; { CREATE_AUTHOR_INDEX } ; { CREATE_ADMIN_CONFIG } ; "
) ) ? ;
// ALTER TABLE does not support IF NOT EXISTS — ignore the error
// when the column already exists (idempotent on re-runs).
let _ = conn . execute ( ALTER_QUOTES_ADD_HIDDEN , [ ] ) ;
Ok ( ( ) )
} )
. await
@ -102,7 +108,8 @@ impl QuoteRepository for NativeRepository {
const PAGE_SIZE : i64 = 10 ;
// ── Build WHERE clause ────────────────────────────────────
let mut conditions : Vec < String > = Vec ::new ( ) ;
// Always exclude hidden quotes from listing endpoints.
let mut conditions : Vec < String > = vec! [ "q.hidden = 0" . to_owned ( ) ] ;
if author . is_some ( ) {
conditions . push ( "q.author = ? COLLATE NOCASE" . to_owned ( ) ) ;
}
@ -120,11 +127,7 @@ impl QuoteRepository for NativeRepository {
if date_before . is_some ( ) {
conditions . push ( "q.date <= ?" . to_owned ( ) ) ;
}
let where_clause = if conditions . is_empty ( ) {
String ::new ( )
} else {
format! ( "WHERE {}" , conditions . join ( " AND " ) )
} ;
let where_clause = format! ( "WHERE {}" , conditions . join ( " AND " ) ) ;
// Collect bound params in order for both queries
let mut params : Vec < Box < dyn rusqlite ::types ::ToSql > > = Vec ::new ( ) ;
@ -158,7 +161,7 @@ impl QuoteRepository for NativeRepository {
// ── Fetch the page of quotes ──────────────────────────────
let list_sql = format! (
" SELECT q . id , q . text , q . author , q . source , q . date , \
q . created_at, q . updated_at \
q . hidden, q . created_at, q . updated_at \
FROM quotes q { where_clause } \
ORDER BY q . created_at DESC \
LIMIT ? OFFSET ? "
@ -189,14 +192,16 @@ impl QuoteRepository for NativeRepository {
. query_map (
rusqlite ::params_from_iter ( param_refs2 . iter ( ) . copied ( ) ) ,
| row | {
let hidden_int : i64 = row . get ( 5 ) ? ;
Ok ( Quote {
id : row . get ( 0 ) ? ,
text : row . get ( 1 ) ? ,
author : row . get ( 2 ) ? ,
source : row . get ( 3 ) ? ,
date : row . get ( 4 ) ? ,
created_at : row . get ( 5 ) ? ,
updated_at : row . get ( 6 ) ? ,
hidden : hidden_int ! = 0 ,
created_at : row . get ( 6 ) ? ,
updated_at : row . get ( 7 ) ? ,
tags : vec ! [ ] ,
} )
} ,
@ -231,7 +236,7 @@ impl QuoteRepository for NativeRepository {
self . conn
. call ( move | conn | {
let mut stmt = conn . prepare (
" SELECT id , text , author , source , date , created_at, updated_at \
" SELECT id , text , author , source , date , hidden, created_at, updated_at \
FROM quotes WHERE id = ? " ,
) ? ;
let mut rows = stmt . query ( [ & id as & str ] ) ? ;
@ -254,8 +259,8 @@ impl QuoteRepository for NativeRepository {
self . conn
. call ( | conn | {
let mut stmt = conn . prepare (
" SELECT id , text , author , source , date , created_at, updated_at \
FROM quotes ORDER BY RANDOM ( ) LIMIT 1 " ,
" SELECT id , text , author , source , date , hidden, created_at, updated_at \
FROM quotes WHERE hidden = 0 ORDER BY RANDOM ( ) LIMIT 1 " ,
) ? ;
let mut rows = stmt . query ( [ ] ) ? ;
match rows . next ( ) ? {
@ -307,7 +312,7 @@ impl QuoteRepository for NativeRepository {
// Read back the inserted row to obtain server-generated timestamps
let mut stmt = conn . prepare (
" SELECT id , text , author , source , date , created_at, updated_at \
" SELECT id , text , author , source , date , hidden, created_at, updated_at \
FROM quotes WHERE id = ? " ,
) ? ;
let mut rows = stmt . query ( [ & id2 as & str ] ) ? ;
@ -377,6 +382,9 @@ impl QuoteRepository for NativeRepository {
}
sets . push ( "source = ?" . to_owned ( ) ) ;
sets . push ( "date = ?" . to_owned ( ) ) ;
if input . hidden . is_some ( ) {
sets . push ( "hidden = ?" . to_owned ( ) ) ;
}
sets . push ( "updated_at = datetime('now')" . to_owned ( ) ) ;
let sql = format! ( "UPDATE quotes SET {} WHERE id = ?" , sets . join ( ", " ) ) ;
@ -392,6 +400,9 @@ impl QuoteRepository for NativeRepository {
// source and date may be null (None clears the field)
params . push ( Box ::new ( input . source . clone ( ) ) ) ;
params . push ( Box ::new ( input . date . clone ( ) ) ) ;
if let Some ( h ) = input . hidden {
params . push ( Box ::new ( if h { 1 i64 } else { 0 i64 } ) ) ;
}
params . push ( Box ::new ( id . clone ( ) ) ) ;
let param_refs : Vec < & dyn rusqlite ::types ::ToSql > =
@ -412,7 +423,7 @@ impl QuoteRepository for NativeRepository {
// Read back the updated quote
let mut stmt = conn . prepare (
" SELECT id , text , author , source , date , created_at, updated_at \
" SELECT id , text , author , source , date , hidden, created_at, updated_at \
FROM quotes WHERE id = ? " ,
) ? ;
let mut rows = stmt . query ( [ & id as & str ] ) ? ;
@ -815,6 +826,7 @@ mod tests {
source : None ,
date : None ,
tags : Some ( vec! [ "new" . to_owned ( ) ] ) ,
hidden : None ,
} ,
& auth ,
)