a562dae1e3
The seasonal peak subquery used a correlated SELECT inside a GROUP BY, causing a full nightly_cache scan per object (210-270s for 14k objects). Replaced with a simple MAX() GROUP BY — now instant. Also added three indexes on nightly_cache(night_date) that were missing and causing all dashboard queries to run 2+ second full table scans. Replaced 🔴 emoji in night mode buttons with CSS circles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
81 lines
2.8 KiB
Rust
81 lines
2.8 KiB
Rust
use anyhow::Context;
|
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
|
use sqlx::SqlitePool;
|
|
use std::str::FromStr;
|
|
|
|
pub async fn init_db(database_url: &str) -> anyhow::Result<SqlitePool> {
|
|
let options = SqliteConnectOptions::from_str(database_url)
|
|
.context("invalid DATABASE_URL")?
|
|
.create_if_missing(true)
|
|
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
|
.foreign_keys(true);
|
|
|
|
let pool = SqlitePoolOptions::new()
|
|
.max_connections(5)
|
|
.connect_with(options)
|
|
.await
|
|
.context("failed to connect to SQLite")?;
|
|
|
|
run_schema(&pool).await?;
|
|
run_migrations(&pool).await?;
|
|
seed_horizon(&pool).await?;
|
|
|
|
Ok(pool)
|
|
}
|
|
|
|
async fn run_schema(pool: &SqlitePool) -> anyhow::Result<()> {
|
|
let schema = include_str!("schema.sql");
|
|
// Execute each statement separately
|
|
for statement in schema.split(';') {
|
|
let s = statement.trim();
|
|
if !s.is_empty() {
|
|
sqlx::query(s).execute(pool).await?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Additive migrations for columns added after initial schema creation.
|
|
/// SQLite doesn't support IF NOT EXISTS for ADD COLUMN, so we check the error and ignore it.
|
|
async fn run_migrations(pool: &SqlitePool) -> anyhow::Result<()> {
|
|
let migrations: &[&str] = &[
|
|
"ALTER TABLE nightly_cache ADD COLUMN is_visible_tonight INTEGER DEFAULT 0",
|
|
"ALTER TABLE catalog ADD COLUMN caldwell_num INTEGER",
|
|
"ALTER TABLE catalog ADD COLUMN arp_num INTEGER",
|
|
"ALTER TABLE catalog ADD COLUMN melotte_num INTEGER",
|
|
"ALTER TABLE catalog ADD COLUMN collinder_num INTEGER",
|
|
// Performance indexes for nightly_cache date-range queries
|
|
"CREATE INDEX IF NOT EXISTS idx_nc_date ON nightly_cache(night_date)",
|
|
"CREATE INDEX IF NOT EXISTS idx_nc_date_alt ON nightly_cache(night_date, max_alt_deg)",
|
|
"CREATE INDEX IF NOT EXISTS idx_nc_catalog_date ON nightly_cache(catalog_id, night_date)",
|
|
];
|
|
for sql in migrations {
|
|
match sqlx::query(sql).execute(pool).await {
|
|
Ok(_) => tracing::info!("Migration applied: {}", &sql[..sql.len().min(60)]),
|
|
Err(e) if e.to_string().contains("duplicate column") => {}
|
|
Err(e) => tracing::warn!("Migration skipped ({}): {}", sql, e),
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn seed_horizon(pool: &SqlitePool) -> anyhow::Result<()> {
|
|
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM horizon")
|
|
.fetch_one(pool)
|
|
.await?;
|
|
|
|
if count == 0 {
|
|
let mut tx = pool.begin().await?;
|
|
for az in 0..360i32 {
|
|
sqlx::query("INSERT OR IGNORE INTO horizon (az_deg, alt_deg) VALUES (?, 15.0)")
|
|
.bind(az)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
}
|
|
tx.commit().await?;
|
|
tracing::info!("Seeded horizon table with 360 flat points at 15°");
|
|
}
|
|
|
|
Ok(())
|
|
}
|