Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/utopia-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ struct ManifestDataDir {
/// not a side effect of a code change.
// 是迁移文件的**个数**,不是最大的编号(守卫 `schema_version_policy_compares_against_current`
// 按个数比):编号有空缺时两者不同——0071 由一个开放 PR 占着,0072 先落,个数是 71
const CURRENT_SCHEMA_VERSION: u32 = 111;
const CURRENT_SCHEMA_VERSION: u32 = 112;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
Expand Down
5 changes: 3 additions & 2 deletions crates/utopia-server/src/api/mcp_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1008,10 +1008,11 @@ async fn entity_facts_reads_back_a_statements_role_words() -> anyhow::Result<()>
return Ok(());
};
sqlx::query(
"INSERT INTO statement_qualifiers(fact_id,role,value)
VALUES ($1,'lines','{\"value\":\"10-42\"}'::jsonb)",
"INSERT INTO statement_qualifiers(fact_id,role,value,chunk_id)
VALUES ($1,'lines','{\"value\":\"10-42\"}'::jsonb,$2)",
)
.bind(f.corrected)
.bind(f.chunk)
.execute(&f.state.pool)
.await?;
let result = f
Expand Down
1 change: 1 addition & 0 deletions crates/utopia-server/src/extraction_open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ async fn apply_open_extraction(
utopia_store::graph::add_statement_qualifier(
pool,
fact_id,
chunk.id,
role,
value.as_ref(),
*entity,
Expand Down
8 changes: 8 additions & 0 deletions crates/utopia-store/src/export_provenance_integrity.sql
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ SELECT edge, kind, COUNT(*) AS rows FROM (
LEFT JOIN entities e ON e.id = q.entity_id
WHERE f.kb_id = $1 AND q.entity_id IS NOT NULL
UNION ALL
-- @edge statement_qualifiers.chunk_id -> chunks.id
-- 角色词记在哪一块上:归属同样按所属 fact 的库判
SELECT 'squalifier.chunk', 'cross_kb', c.kb_id IS DISTINCT FROM f.kb_id
FROM statement_qualifiers q
JOIN facts f ON f.id = q.fact_id
LEFT JOIN chunks c ON c.id = q.chunk_id
WHERE f.kb_id = $1
UNION ALL
-- @edge time_mentions.fact_id -> facts.id
-- 时间提及以行自己的 kb 归属:指的事实与段落都必须同库
SELECT 'timemention.fact', 'cross_kb', f.kb_id IS DISTINCT FROM t.kb_id
Expand Down
16 changes: 9 additions & 7 deletions crates/utopia-store/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,12 @@ pub async fn fact_qualifiers_for(
Ok(out)
}

/// 往一条开放陈述上写一个按角色词记的属性。**先写者留着**:同一条陈述同一个角色词
/// 再听到一次不覆盖(与 [`upsert_fact_qualifier`] 的 Conflict 一条规矩——两次观察
/// 不一致该另立一行加一条冲突,这一刀还没走到那一步)。值与实体必须恰好给一个
/// 往一条开放陈述上写一个按角色词记的属性,记在说它的那一块上(0070 决定 1)。
/// 同一块再说一次不写新行;另一块说的另记一行,旧行不动。值与实体必须恰好给一个
pub async fn add_statement_qualifier(
pool: &PgPool,
fact_id: Uuid,
chunk_id: Uuid,
role: &str,
value: Option<&serde_json::Value>,
entity_id: Option<Uuid>,
Expand All @@ -223,19 +223,21 @@ pub async fn add_statement_qualifier(
));
}
sqlx::query(
"INSERT INTO statement_qualifiers (fact_id, role, value, entity_id)
VALUES ($1, $2, $3, $4) ON CONFLICT (fact_id, role) DO NOTHING",
"INSERT INTO statement_qualifiers (fact_id, role, value, entity_id, chunk_id)
VALUES ($1, $2, $3, $4, $5) ON CONFLICT (fact_id, role, chunk_id) DO NOTHING",
)
.bind(fact_id)
.bind(role)
.bind(value)
.bind(entity_id)
.bind(chunk_id)
.execute(pool)
.await?;
Ok(())
}

/// 一批开放陈述各自的角色词属性,按事实 id 取回;实体那一格连上显示名
/// 一批开放陈述各自的角色词属性,按事实 id 取回;实体那一格连上显示名。
/// 同一个值记在几块上只返回一次
pub async fn statement_qualifiers_for(
pool: &PgPool,
fact_ids: &[Uuid],
Expand All @@ -253,7 +255,7 @@ pub async fn statement_qualifiers_for(
entity_name: Option<String>,
}
let rows: Vec<Row> = sqlx::query_as(
"SELECT q.fact_id, q.role, q.value, q.entity_id, e.canonical_name AS entity_name
"SELECT DISTINCT q.fact_id, q.role, q.value, q.entity_id, e.canonical_name AS entity_name
FROM statement_qualifiers q
LEFT JOIN entities e ON e.id = q.entity_id
WHERE q.fact_id = ANY($1)
Expand Down
24 changes: 14 additions & 10 deletions crates/utopia-store/src/materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ async fn materialize_in_tx(
.bind(d.statement)
.execute(&mut **tx)
.await?;
// 证据与限定各抄一份:证据是同一段原文的同一处引文;限定照角色词原样带过去
// 证据每次都抄:证据是同一段原文的同一处引文
sqlx::query(
"INSERT INTO fact_evidence (fact_id, chunk_id, quote, document_id, doc_version,
proposed_predicate, quote_start, quote_end)
Expand All @@ -423,15 +423,19 @@ async fn materialize_in_tx(
.bind(d.statement)
.execute(&mut **tx)
.await?;
sqlx::query(
"INSERT INTO statement_qualifiers (fact_id, role, value, entity_id)
SELECT $1, role, value, entity_id FROM statement_qualifiers WHERE fact_id = $2
ON CONFLICT DO NOTHING",
)
.bind(fact)
.bind(d.statement)
.execute(&mut **tx)
.await?;
if new {
// 角色词只在建行时抄(0070 决定 1 的修订):并进已有行的陈述可能说了别的值,
// 抄过来这一行就同时读到两个值;别的值由决定 4 带过来
sqlx::query(
"INSERT INTO statement_qualifiers (fact_id, role, value, entity_id, chunk_id)
SELECT $1, role, value, entity_id, chunk_id FROM statement_qualifiers WHERE fact_id = $2
ON CONFLICT DO NOTHING",
)
.bind(fact)
.bind(d.statement)
.execute(&mut **tx)
.await?;
}
}
// 3a. 已批准的规则算隐含行(0044 决定 3 第五片)。读数只查缓存:缓存里没有的这一轮
// 不算,`read_phrases` 填上之后再来。短语规则按陈述触发,类别词规则按实体触发
Expand Down
3 changes: 2 additions & 1 deletion crates/utopia-store/src/pending.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,8 @@ async fn confirm_open(
} else {
q.get("value")
};
crate::graph::add_statement_qualifier(pool, fact_id, role, value, entity_id).await?;
crate::graph::add_statement_qualifier(pool, fact_id, v.chunk_id, role, value, entity_id)
.await?;
}
for t in json_items(v.time_words.as_ref()) {
let (Some(text), Some(char_start)) = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! 一条陈述「Harbor Bakery —is based in→ Port Ellen」,签名 (is based in, organization,
//! place) 绑到 headquartered_in:算出一条类型化行,谓词是它,证据与限定抄过来,
//! `from_statement_id` 指回陈述;再跑一遍什么都不动;带 mood 的陈述不算;另一份文档
//! 的同一句并进同一行(两条来源、两条证据);作废其中一条陈述,行还在、来源少一条;
//! 的同一句并进同一行(两条来源、两条证据),它说的另一个 since 不跟进来,行只读建行那条的;作废其中一条陈述,行还在、来源少一条;
//! 绑定翻成 reverse,旧行作废、新行主宾对调;绑定改成 none,行作废、不再补。
//!
//! 没有 `UTOPIA_DATABASE_URL` 时跳过而不是失败。自建自拆,绝不碰已有的库。
Expand Down Expand Up @@ -133,21 +133,29 @@ async fn a_bound_statement_becomes_a_typed_fact() -> anyhow::Result<()> {
.bind(doc)
.execute(&pool)
.await?;
anyhow::Ok(())
anyhow::Ok(chunk)
}
};
evidence_of(stated, "bakery.txt").await?;
evidence_of(again, "bakery-2.txt").await?;
sqlx::query(
"INSERT INTO statement_qualifiers (fact_id, role, value) VALUES ($1, 'since', '\"2019\"'::jsonb)",
)
.bind(stated)
.execute(&pool)
.await?;
let stated_chunk = evidence_of(stated, "bakery.txt").await?;
let again_chunk = evidence_of(again, "bakery-2.txt").await?;
let planned_chunk = evidence_of(planned, "bakery-plan.txt").await?;
for (statement, chunk, since) in [(stated, stated_chunk, "2019"), (again, again_chunk, "2021")] {
sqlx::query(
"INSERT INTO statement_qualifiers (fact_id, role, value, chunk_id)
VALUES ($1, 'since', to_jsonb($3::text), $2)",
)
.bind(statement)
.bind(chunk)
.bind(since)
.execute(&pool)
.await?;
}
sqlx::query(
"INSERT INTO statement_qualifiers (fact_id, role, value) VALUES ($1, 'mood', '\"will\"'::jsonb)",
"INSERT INTO statement_qualifiers (fact_id, role, value, chunk_id)
VALUES ($1, 'mood', '\"will\"'::jsonb, $2)",
)
.bind(planned)
.bind(planned_chunk)
.execute(&pool)
.await?;

Expand Down Expand Up @@ -207,13 +215,17 @@ async fn a_bound_statement_becomes_a_typed_fact() -> anyhow::Result<()> {
.bind(typed_id)
.fetch_one(&pool)
.await?;
let qualifiers: Vec<String> = sqlx::query_scalar(
"SELECT role FROM statement_qualifiers WHERE fact_id = $1 ORDER BY role",
// 并进来的那条说 since 2021:不抄,行只读建行那条的 2019
let qualifiers: Vec<(String, String, Uuid)> = sqlx::query_as(
"SELECT role, value #>> '{}', chunk_id FROM statement_qualifiers WHERE fact_id = $1",
)
.bind(typed_id)
.fetch_all(&pool)
.await?;
assert_eq!((sources, evidence, qualifiers), (3, 2, vec!["since".to_string()]));
assert_eq!(
(sources, evidence, qualifiers),
(3, 2, vec![("since".to_string(), "2019".to_string(), stated_chunk)])
);

// 再跑一遍:什么都不动
assert_eq!(materialize(&pool, kb).await?, Outcome::default());
Expand Down
Loading
Loading