diff --git a/crates/idx-core/src/investor.rs b/crates/idx-core/src/investor.rs index 15c03a8..001fdaa 100644 --- a/crates/idx-core/src/investor.rs +++ b/crates/idx-core/src/investor.rs @@ -555,7 +555,7 @@ impl FromStr for HolderSource { "all" => Ok(Self::All), "ksei" => Ok(Self::Ksei), "bing" => Ok(Self::Bing), - _ => Err(IdxError::ParseError( + _ => Err(IdxError::InvalidInput( "invalid --source, expected: all|ksei|bing".to_string(), )), } @@ -588,7 +588,7 @@ impl FromStr for ConcentrationRank { "top1" => Ok(Self::Top1), "top3" => Ok(Self::Top3), "hhi" => Ok(Self::Hhi), - _ => Err(IdxError::ParseError( + _ => Err(IdxError::InvalidInput( "invalid --by, expected: top1|top3|hhi".to_string(), )), } diff --git a/crates/idx-core/src/ownership/db.rs b/crates/idx-core/src/ownership/db.rs index bafa263..7668c6b 100644 --- a/crates/idx-core/src/ownership/db.rs +++ b/crates/idx-core/src/ownership/db.rs @@ -797,7 +797,7 @@ pub fn query_releases(conn: &Connection) -> Result, IdxErr .prepare( "SELECT id, source_url, sha256, as_of_date, row_count, imported_at FROM ownership_releases - ORDER BY as_of_date DESC, imported_at DESC", + ORDER BY as_of_date DESC, imported_at DESC, id DESC", ) .map_err(|e| IdxError::DatabaseError(e.to_string()))?; @@ -1298,7 +1298,7 @@ fn latest_ksei_release(conn: &Connection) -> Result, I conn.query_row( "SELECT sha256, as_of_date FROM ownership_releases - ORDER BY as_of_date DESC, imported_at DESC + ORDER BY as_of_date DESC, imported_at DESC, id DESC LIMIT 1", [], |row| { @@ -1544,6 +1544,39 @@ mod tests { ); } + #[test] + fn same_second_imports_resolve_to_the_later_file_everywhere() { + // Original and corrected file for one date imported in the same + // second: the ticker view and `changes` must read the same one. + let conn = setup(); + let bbca = upsert_ticker(&conn, "BBCA", None).unwrap(); + let alpha = entity(&conn, "ALPHA"); + write_release( + &conn, + "jan", + "2026-01-30", + 7, + bbca, + &[(Some(alpha), "ALPHA", 1000)], + ); + write_release( + &conn, + "jan-fix", + "2026-01-30", + 7, + bbca, + &[(Some(alpha), "ALPHA", 1200)], + ); + + let ticker = query_ticker_holdings(&conn, "BBCA").unwrap(); + + assert_eq!( + ticker.holders[0].percentage_bps, 1200, + "{:?}", + ticker.holders + ); + } + #[test] fn changes_reject_a_date_without_a_release() { let conn = setup(); diff --git a/crates/idx-core/src/ownership/graph.rs b/crates/idx-core/src/ownership/graph.rs index c295376..d51fd12 100644 --- a/crates/idx-core/src/ownership/graph.rs +++ b/crates/idx-core/src/ownership/graph.rs @@ -182,7 +182,7 @@ fn query_all_edges(conn: &Connection) -> Result, IdxError> { if let Ok(release_sha) = conn.query_row( "SELECT sha256 FROM ownership_releases - ORDER BY as_of_date DESC, imported_at DESC + ORDER BY as_of_date DESC, imported_at DESC, id DESC LIMIT 1", [], |row| row.get::<_, String>(0), diff --git a/crates/idx-core/src/ownership/search.rs b/crates/idx-core/src/ownership/search.rs index 40fd1f1..9b0af1e 100644 --- a/crates/idx-core/src/ownership/search.rs +++ b/crates/idx-core/src/ownership/search.rs @@ -60,6 +60,19 @@ pub fn search_entities( .collect() } +/// Former name of [`search_entities`], kept for library users. +#[deprecated(since = "0.3.3", note = "use search_entities")] +pub fn fts_search(conn: &Connection, query: &str, limit: usize) -> Result, IdxError> { + search_entities(conn, query, limit) +} + +/// Search no longer uses an FTS index, so there is nothing to rebuild; kept +/// for library users. +#[deprecated(since = "0.3.3", note = "search reads entities and aliases directly")] +pub fn rebuild_fts(_conn: &Connection) -> Result<(), IdxError> { + Ok(()) +} + #[cfg(test)] mod tests { use rusqlite::{Connection, params}; @@ -124,6 +137,15 @@ mod tests { ); } + #[test] + fn a_name_prefix_ranks_above_a_mid_name_match() { + // Neither is an exact name or alias: only prefix vs substring differs, + // and the substring match is shorter, so length alone would flip them. + let conn = conn_with(&[("BANK MANDIRI TASPEN", &[]), ("PT BANK", &[])]); + + assert_eq!(names(&conn, "bank"), ["BANK MANDIRI TASPEN", "PT BANK"]); + } + #[test] fn treats_like_wildcards_literally_and_empty_queries_as_no_match() { let conn = conn_with(&[("ALPHA_FUND", &[]), ("ALPHAXFUND", &[]), ("BETA 100%", &[])]); diff --git a/scripts/build-ownership-snapshot.sh b/scripts/build-ownership-snapshot.sh index 997b70b..b423c7f 100755 --- a/scripts/build-ownership-snapshot.sh +++ b/scripts/build-ownership-snapshot.sh @@ -79,9 +79,9 @@ fi mkdir -p "$OUTPUT_DIR" -LATEST_AS_OF="$(sqlite3 "$DB_PATH" "SELECT as_of_date FROM ownership_releases ORDER BY as_of_date DESC, imported_at DESC LIMIT 1;")" -LATEST_RELEASE_SHA="$(sqlite3 "$DB_PATH" "SELECT sha256 FROM ownership_releases ORDER BY as_of_date DESC, imported_at DESC LIMIT 1;")" -LATEST_ROW_COUNT="$(sqlite3 "$DB_PATH" "SELECT row_count FROM ownership_releases ORDER BY as_of_date DESC, imported_at DESC LIMIT 1;")" +LATEST_AS_OF="$(sqlite3 "$DB_PATH" "SELECT as_of_date FROM ownership_releases ORDER BY as_of_date DESC, imported_at DESC, id DESC LIMIT 1;")" +LATEST_RELEASE_SHA="$(sqlite3 "$DB_PATH" "SELECT sha256 FROM ownership_releases ORDER BY as_of_date DESC, imported_at DESC, id DESC LIMIT 1;")" +LATEST_ROW_COUNT="$(sqlite3 "$DB_PATH" "SELECT row_count FROM ownership_releases ORDER BY as_of_date DESC, imported_at DESC, id DESC LIMIT 1;")" RELEASE_COUNT="$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM ownership_releases;")" TICKER_COUNT="$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM tickers;")"