feat: backup architecture docs, source code, and scripts
This commit is contained in:
@@ -77,6 +77,8 @@ pub struct VideoRow {
|
||||
pub status: String,
|
||||
pub user_id: Option<i32>,
|
||||
pub job_id: Option<i32>,
|
||||
pub created_at: Option<String>,
|
||||
pub registration_time: Option<String>,
|
||||
}
|
||||
|
||||
impl From<VideoRow> for VideoRecord {
|
||||
@@ -103,7 +105,8 @@ impl From<VideoRow> for VideoRecord {
|
||||
status: VideoStatus::from_db_str(&row.status).unwrap_or(VideoStatus::Pending),
|
||||
user_id: row.user_id.map(|v| v as i64),
|
||||
job_id: row.job_id.map(|v| v as i64),
|
||||
created_at: String::new(),
|
||||
created_at: row.created_at.unwrap_or_default(),
|
||||
registration_time: row.registration_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,6 +127,7 @@ pub struct VideoRecord {
|
||||
pub user_id: Option<i64>,
|
||||
pub job_id: Option<i64>,
|
||||
pub created_at: String,
|
||||
pub registration_time: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -701,7 +705,7 @@ impl PostgresDb {
|
||||
let table = schema::table_name("videos");
|
||||
let result = sqlx::query_as::<_, VideoRow>(
|
||||
&format!(
|
||||
"SELECT id, uuid, file_path, file_name, duration, width, height, fps, probe_json, fs_video, fs_json, psql_chunk, pobject_chunk, mobject_chunk, pvector_chunk, qvector_chunk, status, user_id, job_id FROM {} WHERE uuid = $1",
|
||||
"SELECT id, uuid, file_path, file_name, duration, width, height, fps, probe_json, fs_video, fs_json, psql_chunk, pobject_chunk, mobject_chunk, pvector_chunk, qvector_chunk, status, user_id, job_id, created_at::text, registration_time::text FROM {} WHERE uuid = $1",
|
||||
table
|
||||
)
|
||||
)
|
||||
@@ -796,28 +800,90 @@ impl PostgresDb {
|
||||
}
|
||||
|
||||
pub async fn list_videos(&self, limit: i32, offset: i64) -> Result<(Vec<VideoRecord>, i64)> {
|
||||
// Default to unprocessed (status != 'ready')
|
||||
self.search_videos(None, Some(false), limit, offset).await
|
||||
}
|
||||
|
||||
pub async fn search_videos(
|
||||
&self,
|
||||
query: Option<&str>,
|
||||
is_processed: Option<bool>,
|
||||
limit: i32,
|
||||
offset: i64,
|
||||
) -> Result<(Vec<VideoRecord>, i64)> {
|
||||
let table = schema::table_name("videos");
|
||||
|
||||
// Build status condition
|
||||
// is_processed = Some(true) => status = 'ready'
|
||||
// is_processed = Some(false) => status != 'ready'
|
||||
// is_processed = None => no filter
|
||||
let status_cond = match is_processed {
|
||||
Some(true) => "AND status = 'ready'",
|
||||
Some(false) => "AND status != 'ready'",
|
||||
None => "",
|
||||
};
|
||||
|
||||
// Count total
|
||||
let count: Option<i64> = sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {}", table))
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
let total = count.unwrap_or(0);
|
||||
// Build search condition safely
|
||||
// If query is Some, we filter by filename/path/probe_json
|
||||
let search_cond = if query.is_some() {
|
||||
"AND (LOWER(file_name) LIKE $1 OR LOWER(file_path) LIKE $1 OR LOWER(probe_json::text) LIKE $1)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
// Select paged
|
||||
let rows = sqlx::query_as::<_, VideoRow>(
|
||||
&format!(
|
||||
"SELECT id, uuid, file_path, file_name, duration, width, height, fps, probe_json, fs_video, fs_json, psql_chunk, pobject_chunk, mobject_chunk, pvector_chunk, qvector_chunk, status, user_id, job_id FROM {} ORDER BY id DESC LIMIT $1 OFFSET $2",
|
||||
table
|
||||
)
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
let where_clause = format!("WHERE 1=1 {} {}", status_cond, search_cond);
|
||||
|
||||
// 1. Count Query
|
||||
// If query is present, $1 is the pattern.
|
||||
// If query is None, no pattern param needed for count?
|
||||
// Actually, to keep code simple, let's just construct the query string.
|
||||
// SQLx query_as requires bind count to match placeholders.
|
||||
|
||||
let count_query = format!("SELECT COUNT(*) FROM {} {}", table, where_clause);
|
||||
|
||||
let total: i64 = if let Some(q) = query {
|
||||
let pattern = format!("%{}%", q.to_lowercase());
|
||||
sqlx::query_scalar(&count_query)
|
||||
.bind(&pattern)
|
||||
.fetch_one(&self.pool)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_scalar(&count_query)
|
||||
.fetch_one(&self.pool)
|
||||
.await?
|
||||
};
|
||||
|
||||
// 2. Select Query
|
||||
// Cast created_at and registration_time to text
|
||||
let columns = "id, uuid, file_path, file_name, duration, width, height, fps, probe_json, fs_video, fs_json, psql_chunk, pobject_chunk, mobject_chunk, pvector_chunk, qvector_chunk, status, user_id, job_id, created_at::text, registration_time::text";
|
||||
|
||||
// Determine parameter order for LIMIT/OFFSET
|
||||
// If search is present, pattern is $1. Limit is $2. Offset is $3.
|
||||
// If search is not present, Limit is $1. Offset is $2.
|
||||
|
||||
let select_query = if query.is_some() {
|
||||
format!("SELECT {} FROM {} {} ORDER BY id DESC LIMIT $2 OFFSET $3", columns, table, where_clause)
|
||||
} else {
|
||||
format!("SELECT {} FROM {} {} ORDER BY id DESC LIMIT $1 OFFSET $2", columns, table, where_clause)
|
||||
};
|
||||
|
||||
let rows = if let Some(q) = query {
|
||||
let pattern = format!("%{}%", q.to_lowercase());
|
||||
sqlx::query_as::<_, VideoRow>(&select_query)
|
||||
.bind(&pattern)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as::<_, VideoRow>(&select_query)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await?
|
||||
};
|
||||
|
||||
let videos: Vec<VideoRecord> = rows.into_iter().map(|r| r.into()).collect();
|
||||
|
||||
Ok((videos, total))
|
||||
}
|
||||
|
||||
@@ -850,6 +916,19 @@ impl PostgresDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_registration_time(&self, uuid: &str) -> Result<()> {
|
||||
let table = schema::table_name("videos");
|
||||
sqlx::query(&format!(
|
||||
"UPDATE {} SET registration_time = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE uuid = $1 AND registration_time IS NULL",
|
||||
table
|
||||
))
|
||||
.bind(uuid)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_video(&self, uuid: &str) -> Result<()> {
|
||||
tracing::info!("[PostgresDb] Deleting video: {}", uuid);
|
||||
|
||||
|
||||
68
src/core/db/schema_ctx.rs
Normal file
68
src/core/db/schema_ctx.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use anyhow::Result;
|
||||
use sqlx::PgPool;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
/// Schema context for database operations
|
||||
/// Ensures all queries use the correct schema prefix
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SchemaContext {
|
||||
pub prefix: String,
|
||||
}
|
||||
|
||||
static SCHEMA_INSTANCE: std::sync::OnceLock<SchemaContext> = std::sync::OnceLock::new();
|
||||
static SCHEMA_VERSION: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
impl SchemaContext {
|
||||
/// Initialize schema context from environment
|
||||
pub fn init() -> Self {
|
||||
let schema = std::env::var("DATABASE_SCHEMA").unwrap_or_else(|_| "dev".to_string());
|
||||
let prefix = if schema == "public" {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{}.", schema)
|
||||
};
|
||||
Self { prefix }
|
||||
}
|
||||
|
||||
/// Get the global schema context
|
||||
pub fn global() -> &'static Self {
|
||||
SCHEMA_INSTANCE.get_or_init(|| Self::init())
|
||||
}
|
||||
|
||||
/// Get table name with schema prefix
|
||||
pub fn table(&self, name: &str) -> String {
|
||||
format!("{}{}", self.prefix, name)
|
||||
}
|
||||
|
||||
/// Reload schema context (for testing)
|
||||
pub fn reload() {
|
||||
SCHEMA_VERSION.fetch_add(1, Ordering::SeqCst);
|
||||
// Note: OnceLock can't be reset, so we use a different approach
|
||||
// In production, schema doesn't change at runtime
|
||||
}
|
||||
}
|
||||
|
||||
/// Quick helper to get table name with current schema prefix
|
||||
pub fn t(name: &str) -> String {
|
||||
SchemaContext::global().table(name)
|
||||
}
|
||||
|
||||
/// Check if a table exists in the current schema
|
||||
pub async fn table_exists(pool: &PgPool, table_name: &str) -> Result<bool> {
|
||||
let schema = SchemaContext::global();
|
||||
let schema_name = if schema.prefix.is_empty() {
|
||||
"public".to_string()
|
||||
} else {
|
||||
schema.prefix.trim_end_matches('.').to_string()
|
||||
};
|
||||
|
||||
let query = format!(
|
||||
"SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2)"
|
||||
);
|
||||
let exists: bool = sqlx::query_scalar(&query)
|
||||
.bind(&schema_name)
|
||||
.bind(table_name)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
Reference in New Issue
Block a user