mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
fix(ownership): correct changes, alias search, graph, and name lookups
From a full-repo Codex audit, each reproduced on the synced ownership DB:
- `changes` paired an investor's accounts across releases instead of
comparing totals: KREN's KRESNA PRIMA INVEST went 9.44% -> 13.45% (a
second 4.01% account) but was reported as decreased 944 -> 401 bps.
Sum a holder's accounts per ticker, and read one release per date (the
latest imported, as other queries do), so a corrected file supersedes
the original instead of mixing with it.
- `changes` / get_ownership_changes accepted any dates: `from: ""`
reported all 7,154 holders as new, unknown dates returned no changes.
Dates must be YYYY-MM-DD imported releases with `from` before `to`.
- get_investor_holdings / `entity` with an empty name matched every
investor and returned an arbitrary one ("AMY"); `%` and `_` acted as
LIKE wildcards. Reject empty names; match wildcards literally.
- Alias search never worked: the FTS table declared an `aliases` column
its content table lacks (`no such column: T.aliases`), so searches
fell back to canonical names and a raw KSEI name such as
"PT ASTRA INTERNATIONAL TBK" found nothing. Search names and aliases
directly, ranked exact name, exact alias, prefix, then shorter names.
- `graph` enumerated every path (exponential in depth) and its cycle
check matched substrings, dropping entity:1 when entity:10 was on the
path. Traverse breadth-first with an exact visited set.
- Name normalization removed leading PT tokens from the front of a Vec,
quadratic in hostile names; narrow a slice instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
f5da3b745e
commit
1d3fe5ca88
6 changed files with 437 additions and 196 deletions
|
|
@ -134,11 +134,23 @@ impl Investor {
|
|||
/// Holdings of the entity whose name best matches `name` (shortest
|
||||
/// canonical name containing it).
|
||||
pub fn entity(&self, name: &str) -> Result<EntityHoldings, IdxError> {
|
||||
let name = name.trim();
|
||||
// An empty pattern would match every investor and return an arbitrary one.
|
||||
if name.is_empty() {
|
||||
return Err(IdxError::InvalidInput(
|
||||
"investor name must not be empty".into(),
|
||||
));
|
||||
}
|
||||
let conn = self.connect()?;
|
||||
let pattern = format!("%{}%", name.trim());
|
||||
// `%` and `_` in the name are literal characters, not LIKE wildcards.
|
||||
let escaped = name
|
||||
.replace('\\', "\\\\")
|
||||
.replace('%', "\\%")
|
||||
.replace('_', "\\_");
|
||||
let pattern = format!("%{escaped}%");
|
||||
let entity_id = conn
|
||||
.query_row(
|
||||
"SELECT id FROM entities WHERE canonical_name LIKE ?1 COLLATE NOCASE ORDER BY LENGTH(canonical_name) ASC LIMIT 1",
|
||||
"SELECT id FROM entities WHERE canonical_name LIKE ?1 ESCAPE '\\' COLLATE NOCASE ORDER BY LENGTH(canonical_name) ASC LIMIT 1",
|
||||
rusqlite::params![pattern],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
|
|
@ -147,7 +159,7 @@ impl Investor {
|
|||
}
|
||||
|
||||
pub fn search(&self, query: &str, limit: usize) -> Result<Vec<Entity>, IdxError> {
|
||||
search::fts_search(&*self.connect()?, query, limit)
|
||||
search::search_entities(&*self.connect()?, query, limit)
|
||||
}
|
||||
|
||||
pub fn cross_holders(
|
||||
|
|
@ -175,14 +187,29 @@ impl Investor {
|
|||
db::query_bing_flow(&conn, ticker_id)
|
||||
}
|
||||
|
||||
/// Holder changes between two release dates, optionally for one ticker.
|
||||
/// Holder changes between two release dates (`from` earlier than `to`,
|
||||
/// both imported releases), optionally for one ticker.
|
||||
pub fn changes(
|
||||
&self,
|
||||
from: &str,
|
||||
to: &str,
|
||||
ticker: Option<&str>,
|
||||
) -> Result<Vec<ChangeRow>, IdxError> {
|
||||
let mut rows = db::query_changes(&*self.connect()?, from, to)?;
|
||||
let date = |field: &str, value: &str| {
|
||||
NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d").map_err(|_| {
|
||||
IdxError::InvalidInput(format!(
|
||||
"{field} must be a release date as YYYY-MM-DD (got {value:?})"
|
||||
))
|
||||
})
|
||||
};
|
||||
let (from_date, to_date) = (date("from", from)?, date("to", to)?);
|
||||
if from_date >= to_date {
|
||||
return Err(IdxError::InvalidInput(format!(
|
||||
"from ({from_date}) must be earlier than to ({to_date})"
|
||||
)));
|
||||
}
|
||||
let (from, to) = (from_date.to_string(), to_date.to_string());
|
||||
let mut rows = db::query_changes(&*self.connect()?, &from, &to)?;
|
||||
if let Some(ticker) = ticker {
|
||||
let ticker = ticker.trim().to_uppercase();
|
||||
rows.retain(|row| row.ticker_code == ticker);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue