mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
fixed dublication of flavors
This commit is contained in:
1 parent
4ec3464d99
commit
905bc00c0e
5 files changed
+131
-4
No files matched your search
@@ -36,5 +36,48 @@ pub async fn connect(database_url: &str) -> anyhow::Result<DatabaseConnection> {
|
||||
}
|
||||
}
|
||||
|
||||
ensure_schema(&conn).await?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Idempotent, additive schema patches the Rust backend owns now that Django is
|
||||
/// retired. SQLite has no `ADD COLUMN IF NOT EXISTS`, so we probe `PRAGMA
|
||||
/// table_info` first and only ALTER when the column is missing.
|
||||
async fn ensure_schema(conn: &DatabaseConnection) -> anyhow::Result<()> {
|
||||
add_column_if_missing(
|
||||
conn,
|
||||
"api_flavor",
|
||||
"aliases",
|
||||
"ALTER TABLE api_flavor ADD COLUMN aliases TEXT",
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_column_if_missing(
|
||||
conn: &DatabaseConnection,
|
||||
table: &str,
|
||||
column: &str,
|
||||
alter_sql: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let rows = conn
|
||||
.query_all(Statement::from_string(
|
||||
DatabaseBackend::Sqlite,
|
||||
format!("PRAGMA table_info({table})"),
|
||||
))
|
||||
.await?;
|
||||
let exists = rows
|
||||
.iter()
|
||||
.any(|r| r.try_get::<String>("", "name").as_deref() == Ok(column));
|
||||
if exists {
|
||||
return Ok(());
|
||||
}
|
||||
conn.execute(Statement::from_string(
|
||||
DatabaseBackend::Sqlite,
|
||||
alter_sql.to_owned(),
|
||||
))
|
||||
.await?;
|
||||
tracing::info!("schema: added {table}.{column}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -22,6 +22,13 @@ pub struct Model {
|
||||
pub is_available: bool,
|
||||
pub is_legacy: bool,
|
||||
pub external_id: Option<i64>,
|
||||
/// Alternate names this flavor is known by — populated when another flavor
|
||||
/// (typically a legacy stand-in) is merged into this one. JSON list of
|
||||
/// strings, stored as TEXT. Lets `seed_legacy` avoid re-creating a merged-away
|
||||
/// legacy flavor and lets search match its old name. Added by the guarded
|
||||
/// startup migration in `db.rs`; may be NULL on rows predating it.
|
||||
#[sea_orm(column_type = "Json", nullable)]
|
||||
pub aliases: Option<Json>,
|
||||
/// Django stores naive UTC text ("YYYY-MM-DD HH:MM:SS.ffffff"); we treat it
|
||||
/// as UTC when formatting to DRF's ISO-8601 `...Z` output.
|
||||
pub created_at: ChronoDateTime,
|
||||
|
||||
@@ -131,6 +131,18 @@ impl BackgroundJob for SeedLegacy {
|
||||
}
|
||||
}
|
||||
|
||||
// Don't recreate a legacy flavor that has since been merged into a
|
||||
// (Shopify) flavor — the survivor carries this name as an alias.
|
||||
if existing.is_none() {
|
||||
if let Some(owner) = alias_owner(db, name, cat.id).await? {
|
||||
log.push(format!(
|
||||
"Skipping '{name}' (superseded by '{}')",
|
||||
owner.name
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve image first (so we can set local paths on insert).
|
||||
let mut local_paths: Option<Vec<String>> = None;
|
||||
if let Some(url) = image_url.filter(|s| !s.is_empty()) {
|
||||
@@ -208,6 +220,37 @@ fn pystr(v: Option<&Value>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// A flavor in `category_id` that lists `name` (case-insensitive) among its
|
||||
/// merge aliases, if any. Used to suppress recreating a merged-away legacy row.
|
||||
async fn alias_owner(
|
||||
db: &DatabaseConnection,
|
||||
name: &str,
|
||||
category_id: i32,
|
||||
) -> anyhow::Result<Option<flavor::Model>> {
|
||||
let nl = name.to_lowercase();
|
||||
let candidates = Flavor::find()
|
||||
.filter(flavor::Column::CategoryId.eq(category_id))
|
||||
.filter(flavor::Column::Aliases.is_not_null())
|
||||
.all(db)
|
||||
.await?;
|
||||
Ok(candidates.into_iter().find(|f| {
|
||||
json_str_list(&f.aliases)
|
||||
.iter()
|
||||
.any(|a| a.to_lowercase() == nl)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Django JSONField (list) stored as TEXT → Vec<String>.
|
||||
fn json_str_list(v: &Option<Value>) -> Vec<String> {
|
||||
match v {
|
||||
Some(Value::Array(arr)) => arr
|
||||
.iter()
|
||||
.filter_map(|x| x.as_str().map(String::from))
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn goc_category(
|
||||
db: &DatabaseConnection,
|
||||
name: &str,
|
||||
|
||||
+34
-2
@@ -87,14 +87,46 @@ pub async fn merge_flavors(
|
||||
None
|
||||
};
|
||||
|
||||
// Record the removed flavor's name (and any aliases it already carried) onto
|
||||
// the survivor, so `seed_legacy` won't recreate the merged-away row and search
|
||||
// still resolves the old name. Deduped case-insensitively; never alias the
|
||||
// kept flavor's own name.
|
||||
let original = json_str_list(&keep.aliases);
|
||||
let mut aliases = original.clone();
|
||||
let mut seen: std::collections::HashSet<String> =
|
||||
aliases.iter().map(|a| a.to_lowercase()).collect();
|
||||
seen.insert(keep.name.to_lowercase());
|
||||
for cand in std::iter::once(remove.name.clone()).chain(json_str_list(&remove.aliases)) {
|
||||
if seen.insert(cand.to_lowercase()) {
|
||||
aliases.push(cand);
|
||||
}
|
||||
}
|
||||
let aliases_changed = aliases.len() != original.len();
|
||||
|
||||
Flavor::delete_by_id(remove.id).exec(&txn).await?;
|
||||
|
||||
if let Some(eid) = inherited {
|
||||
if inherited.is_some() || aliases_changed {
|
||||
let mut am: flavor::ActiveModel = keep.clone().into();
|
||||
am.external_id = Set(Some(eid));
|
||||
if let Some(eid) = inherited {
|
||||
am.external_id = Set(Some(eid));
|
||||
}
|
||||
if aliases_changed {
|
||||
am.aliases = Set(Some(serde_json::json!(aliases)));
|
||||
}
|
||||
am.update(&txn).await?;
|
||||
}
|
||||
|
||||
txn.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Django JSONField (list) stored as TEXT → Vec<String>, tolerating null/non-array.
|
||||
fn json_str_list(v: &Option<serde_json::Value>) -> Vec<String> {
|
||||
match v {
|
||||
Some(serde_json::Value::Array(arr)) => arr
|
||||
.iter()
|
||||
.filter_map(|x| x.as_str().map(String::from))
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,8 @@ async fn list(
|
||||
let like = format!("%{w}%");
|
||||
cond = cond
|
||||
.add(flavor::Column::Name.like(&like))
|
||||
.add(flavor::Column::Description.like(&like));
|
||||
.add(flavor::Column::Description.like(&like))
|
||||
.add(flavor::Column::Aliases.like(&like));
|
||||
has = true;
|
||||
}
|
||||
if has {
|
||||
@@ -229,7 +230,8 @@ async fn search(
|
||||
let like = format!("%{w}%");
|
||||
cond = cond
|
||||
.add(flavor::Column::Name.like(&like))
|
||||
.add(flavor::Column::Description.like(&like));
|
||||
.add(flavor::Column::Description.like(&like))
|
||||
.add(flavor::Column::Aliases.like(&like));
|
||||
}
|
||||
q = q.filter(cond);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user