schema.rs: EdgeKeyKind enum (IntegerPair default, TextPair) + archived_field:
Option<&'static str> on EntitySchema. Backward-compat via Default impl.
engine.rs: ddl_edge_table_for() dispatches integer vs text edge DDL.
Existing ddl_edge_table() untouched; new ddl_edge_table_text() adds
src_path/dst_path TEXT columns.
verbs/link.rs (rewrite): dispatches on edge_key_kind. Input JSON
{from:int, to:int} for IntegerPair OR {from:str, to:str} for TextPair.
verbs/rank.rs (rewrite): generic pagerank<K: Hash + Eq + Clone> over
node key type. Same algorithm, polymorphic in key.
verbs/archive.rs (new, 64 LOC): soft-delete via UPDATE <tbl> SET
<archived_field>=1 + optional <archived_field>_at=now(). Schema
without archived_field declared returns VerbError.
Tests: 20/20 kei-entity-store (was 10, +10: 5 text_pair + 5 archive).
kei-task 9/9 preserved (IntegerPair still default, backward-compat
verified).
Enables: kei-sage migration (TextPair edges) + kei-chat-store
migration (archive).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
2.7 KiB
Rust
88 lines
2.7 KiB
Rust
//! `link` verb — INSERT edge into `<edge_table>` (idempotent via
|
|
//! INSERT OR IGNORE). Caller is responsible for higher-level semantic
|
|
//! checks (cycle detection, self-loop) — those live in the sibling
|
|
//! crate (e.g. kei-task::deps).
|
|
//!
|
|
//! Dispatches on `schema.edge_key_kind`:
|
|
//! - `IntegerPair` — input `{from: i64, to: i64, edge_type?}`
|
|
//! - `TextPair` — input `{from: str, to: str, edge_type?}`
|
|
|
|
use crate::error::VerbError;
|
|
use crate::schema::{EdgeKeyKind, EntitySchema};
|
|
use rusqlite::Connection;
|
|
use serde_json::{json, Value};
|
|
|
|
pub fn run(
|
|
conn: &Connection,
|
|
schema: &EntitySchema,
|
|
input: Value,
|
|
) -> Result<Value, VerbError> {
|
|
if !schema.verb_enabled("link") {
|
|
return Err(VerbError::VerbDisabled {
|
|
verb: "link".into(),
|
|
schema: schema.name.into(),
|
|
});
|
|
}
|
|
let edge = schema.edge_table.ok_or_else(|| {
|
|
VerbError::InvalidInput(format!(
|
|
"link: schema {} has no edge_table configured",
|
|
schema.name
|
|
))
|
|
})?;
|
|
let edge_type = input
|
|
.get("edge_type")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("links")
|
|
.to_string();
|
|
|
|
match schema.edge_key_kind {
|
|
EdgeKeyKind::IntegerPair => insert_integer(conn, edge, &input, &edge_type),
|
|
EdgeKeyKind::TextPair => insert_text(conn, edge, &input, &edge_type),
|
|
}
|
|
}
|
|
|
|
fn insert_integer(
|
|
conn: &Connection,
|
|
edge: &str,
|
|
input: &Value,
|
|
edge_type: &str,
|
|
) -> Result<Value, VerbError> {
|
|
let from = input
|
|
.get("from")
|
|
.and_then(|v| v.as_i64())
|
|
.ok_or_else(|| VerbError::InvalidInput("link: missing `from` integer".into()))?;
|
|
let to = input
|
|
.get("to")
|
|
.and_then(|v| v.as_i64())
|
|
.ok_or_else(|| VerbError::InvalidInput("link: missing `to` integer".into()))?;
|
|
conn.execute(
|
|
&format!(
|
|
"INSERT OR IGNORE INTO {edge} (from_id, to_id, edge_type) VALUES (?1, ?2, ?3)"
|
|
),
|
|
rusqlite::params![from, to, edge_type],
|
|
)?;
|
|
Ok(json!({ "ok": true }))
|
|
}
|
|
|
|
fn insert_text(
|
|
conn: &Connection,
|
|
edge: &str,
|
|
input: &Value,
|
|
edge_type: &str,
|
|
) -> Result<Value, VerbError> {
|
|
let from = input
|
|
.get("from")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| VerbError::InvalidInput("link: missing `from` string".into()))?;
|
|
let to = input
|
|
.get("to")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| VerbError::InvalidInput("link: missing `to` string".into()))?;
|
|
conn.execute(
|
|
&format!(
|
|
"INSERT OR IGNORE INTO {edge} (src_path, dst_path, edge_type) VALUES (?1, ?2, ?3)"
|
|
),
|
|
rusqlite::params![from, to, edge_type],
|
|
)?;
|
|
Ok(json!({ "ok": true }))
|
|
}
|