2025-09-05 13:53:10 +00:00
|
|
|
// Copyright (C) 2024 Riften Labs AS
|
|
|
|
|
//
|
|
|
|
|
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
|
|
|
|
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
|
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
// db_utils.rs
|
|
|
|
|
// Copyright (C) 2024 Riften Labs AS
|
|
|
|
|
// AGPL-3.0-or-later
|
|
|
|
|
|
2025-09-05 13:53:10 +00:00
|
|
|
use anyhow::Result;
|
|
|
|
|
use rusqlite::Connection;
|
|
|
|
|
|
|
|
|
|
pub enum CachedSort {
|
|
|
|
|
ScoreDesc,
|
|
|
|
|
ScoreAsc,
|
|
|
|
|
NameAsc,
|
|
|
|
|
NameDesc,
|
|
|
|
|
SymbolAsc,
|
|
|
|
|
SymbolDesc,
|
|
|
|
|
TvlDesc,
|
|
|
|
|
TvlAsc,
|
|
|
|
|
VolumeDesc,
|
|
|
|
|
VolumeAsc,
|
|
|
|
|
Change24hDesc,
|
|
|
|
|
Change24hAsc,
|
|
|
|
|
Change7dDesc,
|
|
|
|
|
Change7dAsc,
|
|
|
|
|
PriceUsdDesc,
|
|
|
|
|
PriceUsdAsc,
|
|
|
|
|
Change24hUsdDesc,
|
|
|
|
|
Change24hUsdAsc,
|
|
|
|
|
Change7dUsdDesc,
|
|
|
|
|
Change7dUsdAsc,
|
|
|
|
|
Apy30dDesc,
|
|
|
|
|
Apy30dAsc,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn order_clause(sort: CachedSort) -> &'static str {
|
|
|
|
|
match sort {
|
2025-09-11 07:36:06 +00:00
|
|
|
// Best first: rank ascending if set; else score/volume fallback.
|
|
|
|
|
CachedSort::ScoreDesc => {
|
|
|
|
|
"ORDER BY (NULLIF(score_rank,0) IS NULL) ASC, \
|
|
|
|
|
NULLIF(score_rank,0) ASC, \
|
|
|
|
|
score DESC, trade_volume DESC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Worst first: rank descending if set; else score/volume fallback.
|
|
|
|
|
CachedSort::ScoreAsc => {
|
|
|
|
|
"ORDER BY (NULLIF(score_rank,0) IS NULL) ASC, \
|
|
|
|
|
NULLIF(score_rank,0) DESC, \
|
|
|
|
|
score ASC, trade_volume ASC, token_id ASC"
|
|
|
|
|
}
|
2025-09-05 13:53:10 +00:00
|
|
|
CachedSort::TvlDesc => "ORDER BY tvl_sats DESC, token_id ASC",
|
2025-09-11 07:36:06 +00:00
|
|
|
CachedSort::TvlAsc => "ORDER BY tvl_sats ASC, token_id ASC",
|
2025-09-05 13:53:10 +00:00
|
|
|
CachedSort::VolumeDesc => "ORDER BY trade_volume DESC, token_id ASC",
|
2025-09-11 07:36:06 +00:00
|
|
|
CachedSort::VolumeAsc => "ORDER BY trade_volume ASC, token_id ASC",
|
|
|
|
|
|
|
|
|
|
// NULLS LAST via leading `IS NULL` (false<true)
|
|
|
|
|
CachedSort::NameAsc => {
|
|
|
|
|
"ORDER BY display_name IS NULL, display_name COLLATE NOCASE ASC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
CachedSort::NameDesc => {
|
|
|
|
|
"ORDER BY display_name IS NULL, display_name COLLATE NOCASE DESC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
CachedSort::SymbolAsc => {
|
|
|
|
|
"ORDER BY display_symbol IS NULL, display_symbol COLLATE NOCASE ASC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
CachedSort::SymbolDesc => {
|
|
|
|
|
"ORDER BY display_symbol IS NULL, display_symbol COLLATE NOCASE DESC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CachedSort::Change24hDesc => {
|
|
|
|
|
"ORDER BY change_24h_bp IS NULL, change_24h_bp DESC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
CachedSort::Change24hAsc => {
|
|
|
|
|
"ORDER BY change_24h_bp IS NULL, change_24h_bp ASC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
CachedSort::Change7dDesc => {
|
|
|
|
|
"ORDER BY change_7d_bp IS NULL, change_7d_bp DESC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
CachedSort::Change7dAsc => {
|
|
|
|
|
"ORDER BY change_7d_bp IS NULL, change_7d_bp ASC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 13:53:10 +00:00
|
|
|
CachedSort::PriceUsdDesc => "ORDER BY price_now_usd DESC, token_id ASC",
|
|
|
|
|
CachedSort::PriceUsdAsc => "ORDER BY price_now_usd ASC, token_id ASC",
|
2025-09-11 07:36:06 +00:00
|
|
|
|
|
|
|
|
CachedSort::Change24hUsdDesc => {
|
|
|
|
|
"ORDER BY change_24h_usd_bp IS NULL, change_24h_usd_bp DESC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
CachedSort::Change24hUsdAsc => {
|
|
|
|
|
"ORDER BY change_24h_usd_bp IS NULL, change_24h_usd_bp ASC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
CachedSort::Change7dUsdDesc => {
|
|
|
|
|
"ORDER BY change_7d_usd_bp IS NULL, change_7d_usd_bp DESC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
CachedSort::Change7dUsdAsc => {
|
|
|
|
|
"ORDER BY change_7d_usd_bp IS NULL, change_7d_usd_bp ASC, token_id ASC"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CachedSort::Apy30dDesc => "ORDER BY apy_30d_bp IS NULL, apy_30d_bp DESC, token_id ASC",
|
|
|
|
|
CachedSort::Apy30dAsc => "ORDER BY apy_30d_bp IS NULL, apy_30d_bp ASC, token_id ASC",
|
2025-09-05 13:53:10 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
/// Create the *current* index set only (no drops).
|
2025-09-05 13:53:10 +00:00
|
|
|
pub fn create_cached_token_metrics_indexes(conn: &Connection) -> Result<()> {
|
|
|
|
|
conn.execute_batch(
|
|
|
|
|
r#"
|
2025-09-11 07:36:06 +00:00
|
|
|
-- Consolidated sort indexes (ASC can be reverse-scanned for DESC; ties may sort in-memory on token_id)
|
2025-09-05 13:53:10 +00:00
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
-- Score / Volume tie-break
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_score
|
|
|
|
|
ON cached_token_metrics(score, trade_volume, token_id);
|
2025-09-05 13:53:10 +00:00
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
-- TVL & Volume single-key sorts
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_tvl
|
|
|
|
|
ON cached_token_metrics(tvl_sats, token_id);
|
2025-09-05 13:53:10 +00:00
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_volume
|
|
|
|
|
ON cached_token_metrics(trade_volume, token_id);
|
|
|
|
|
|
|
|
|
|
-- Price (USD)
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_price_usd
|
|
|
|
|
ON cached_token_metrics(price_now_usd, token_id);
|
2025-09-05 13:53:10 +00:00
|
|
|
|
|
|
|
|
-- APY 30d
|
2025-09-11 07:36:06 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_apy
|
|
|
|
|
ON cached_token_metrics(apy_30d_bp, token_id);
|
|
|
|
|
|
|
|
|
|
-- NULLS LAST expression indexes for text sorts
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_name_ord
|
|
|
|
|
ON cached_token_metrics((display_name IS NULL), display_name COLLATE NOCASE, token_id);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_symbol_ord
|
|
|
|
|
ON cached_token_metrics((display_symbol IS NULL), display_symbol COLLATE NOCASE, token_id);
|
|
|
|
|
|
|
|
|
|
-- % change (BCH)
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_ch24_ord
|
|
|
|
|
ON cached_token_metrics((change_24h_bp IS NULL), change_24h_bp, token_id);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_ch7d_ord
|
|
|
|
|
ON cached_token_metrics((change_7d_bp IS NULL), change_7d_bp, token_id);
|
2025-09-05 13:53:10 +00:00
|
|
|
|
|
|
|
|
-- % change (USD)
|
2025-09-11 07:36:06 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_ch24_usd_ord
|
|
|
|
|
ON cached_token_metrics((change_24h_usd_bp IS NULL), change_24h_usd_bp, token_id);
|
2025-09-05 13:53:10 +00:00
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_ch7d_usd_ord
|
|
|
|
|
ON cached_token_metrics((change_7d_usd_bp IS NULL), change_7d_usd_bp, token_id);
|
2025-09-05 13:53:10 +00:00
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_ctm_score_rank
|
|
|
|
|
ON cached_token_metrics(score_rank, token_id);
|
2025-09-05 13:53:10 +00:00
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
ANALYZE;
|
|
|
|
|
PRAGMA optimize;
|
|
|
|
|
"#,
|
|
|
|
|
)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Indexes that speed up the aggregation path (tx → phe → pool).
|
|
|
|
|
pub fn create_aggregation_path_indexes(conn: &Connection) -> Result<()> {
|
|
|
|
|
conn.execute_batch(
|
|
|
|
|
r#"
|
|
|
|
|
-- Scan tx in a time window, then join to phe by txid
|
2025-09-05 13:53:10 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_tx_effective_ts
|
|
|
|
|
ON tx(effective_timestamp, txid);
|
|
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
-- Join tx → phe on txid
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_phe_txid
|
|
|
|
|
ON pool_history_entry(txid);
|
2025-09-05 13:53:10 +00:00
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
-- Mixed access patterns (pool then txid)
|
2025-09-05 13:53:10 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_phe_pool_txid
|
|
|
|
|
ON pool_history_entry(pool, txid);
|
|
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
-- Time-bounded scans per pool (snapshots/APY)
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_phe_pool_timestamp
|
|
|
|
|
ON pool_history_entry(pool, effective_timestamp);
|
|
|
|
|
|
|
|
|
|
-- Join phe.pool → pool.creation_utxo (and cover token_id)
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_pool_creation_utxo
|
|
|
|
|
ON pool(creation_utxo, token_id);
|
|
|
|
|
|
|
|
|
|
-- Convenience for token-centric lookups
|
2025-09-05 13:53:10 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_pool_token_id
|
|
|
|
|
ON pool(token_id, creation_utxo);
|
|
|
|
|
|
|
|
|
|
ANALYZE;
|
|
|
|
|
PRAGMA optimize;
|
|
|
|
|
"#,
|
|
|
|
|
)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn create_cached_token_metrics_table(conn: &Connection) -> Result<()> {
|
|
|
|
|
conn.execute_batch(
|
|
|
|
|
r#"
|
|
|
|
|
CREATE TABLE IF NOT EXISTS cached_token_metrics (
|
2025-09-11 07:36:06 +00:00
|
|
|
token_id TEXT PRIMARY KEY,
|
|
|
|
|
trade_volume INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
tvl_sats INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
tvl_tokens INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
score INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
score_rank INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
display_name TEXT,
|
|
|
|
|
display_symbol TEXT,
|
|
|
|
|
|
|
|
|
|
-- BCH (sats-per-human) prices
|
|
|
|
|
price_now REAL NOT NULL DEFAULT 0,
|
|
|
|
|
price_24h REAL,
|
|
|
|
|
price_7d REAL,
|
|
|
|
|
change_24h_bp INTEGER,
|
|
|
|
|
change_7d_bp INTEGER,
|
|
|
|
|
|
|
|
|
|
-- USD per human token (from time-matched oracle)
|
|
|
|
|
price_now_usd REAL NOT NULL DEFAULT 0,
|
|
|
|
|
price_24h_usd REAL,
|
|
|
|
|
price_7d_usd REAL,
|
|
|
|
|
change_24h_usd_bp INTEGER,
|
|
|
|
|
change_7d_usd_bp INTEGER,
|
|
|
|
|
|
|
|
|
|
apy_30d_bp INTEGER,
|
|
|
|
|
|
|
|
|
|
last_trade_ts INTEGER,
|
|
|
|
|
updated_at INTEGER NOT NULL
|
2025-09-05 13:53:10 +00:00
|
|
|
);
|
|
|
|
|
"#,
|
|
|
|
|
)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|