Merge M4 — kei-crossdomain migration
This commit is contained in:
commit
91f9f050e1
4 changed files with 90 additions and 17 deletions
1
_primitives/_rust/Cargo.lock
generated
1
_primitives/_rust/Cargo.lock
generated
|
|
@ -1978,6 +1978,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"kei-entity-store",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ name = "kei_crossdomain"
|
|||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
kei-entity-store = { path = "../kei-entity-store" }
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,42 @@
|
|||
//! kei-crossdomain EntitySchema — declarative spec consumed by
|
||||
//! `kei_entity_store::Store` for migrations + user_version pragma.
|
||||
//!
|
||||
//! **Architectural note (2026-04-23 migration to Layer-A engine):**
|
||||
//! kei-crossdomain is an edges-only graph store — URIs (`domain://path`)
|
||||
//! are the only identifiers; there is no primary "node" entity row. The
|
||||
//! engine's `EntitySchema` contract requires exactly one `IntegerPk`
|
||||
//! field, so we declare a minimal synthetic `cross_nodes` table purely
|
||||
//! to satisfy the DDL contract. No code writes to this table; every
|
||||
//! query still runs against `cross_edges`.
|
||||
//!
|
||||
//! **Why `edge_table: None` instead of `Some("cross_edges")` + TextPair:**
|
||||
//! the engine's TextPair edge DDL is `(src_path, dst_path, edge_type)`
|
||||
//! with `PRIMARY KEY(src_path, dst_path, edge_type)` — **incompatible**
|
||||
//! with the existing `cross_edges` schema, which carries five extra
|
||||
//! columns (`id INTEGER PRIMARY KEY`, `weight`, `evidence`, `metadata`,
|
||||
//! `created_at`) and uses column names `from_uri` / `to_uri`. Adopting
|
||||
//! engine's TextPair would destroy the `CrossEdge` type, the
|
||||
//! `link()` rowid return, and backward compatibility with existing
|
||||
//! on-disk DBs. Instead we follow the kei-task pattern: ride the engine
|
||||
//! for connection lifecycle + `PRAGMA user_version` + migration
|
||||
//! orchestration, keep the rich edge DDL in `custom_migrations`.
|
||||
//!
|
||||
//! Constructive path (not pursued here, would require destructive
|
||||
//! rewrite): extend `kei-entity-store` with a richer TextPair variant
|
||||
//! that preserves extra columns via schema fields, OR migrate
|
||||
//! kei-crossdomain callers to drop the id/weight/evidence/metadata
|
||||
//! fields. Both are multi-file changes outside this crate's scope.
|
||||
|
||||
use kei_entity_store::schema::{EdgeKeyKind, EntitySchema, FieldDef};
|
||||
use rusqlite::{Connection, Result};
|
||||
|
||||
pub fn create_schema(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(r#"
|
||||
/// Synthetic primary table — exists solely to satisfy the engine's
|
||||
/// `IntegerPk` requirement. Not used by any verb or caller.
|
||||
static FIELDS: &[FieldDef] = &[FieldDef::pk("id")];
|
||||
|
||||
/// Byte-identical to the pre-migration `cross_edges` DDL (plus the
|
||||
/// three indexes that used to live in the same `execute_batch`).
|
||||
const DDL_CROSS_EDGES: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS cross_edges (
|
||||
id INTEGER PRIMARY KEY,
|
||||
from_uri TEXT NOT NULL,
|
||||
|
|
@ -16,6 +51,32 @@ pub fn create_schema(conn: &Connection) -> Result<()> {
|
|||
CREATE INDEX IF NOT EXISTS idx_ce_from ON cross_edges(from_uri);
|
||||
CREATE INDEX IF NOT EXISTS idx_ce_to ON cross_edges(to_uri);
|
||||
CREATE INDEX IF NOT EXISTS idx_ce_type ON cross_edges(edge_type);
|
||||
"#)?;
|
||||
"#;
|
||||
|
||||
pub static CROSSDOMAIN_SCHEMA: EntitySchema = EntitySchema {
|
||||
name: "crossdomain",
|
||||
table: "cross_nodes",
|
||||
fields: FIELDS,
|
||||
// Empty verb set: every kei-crossdomain op is bespoke (TextPair with
|
||||
// extra columns — engine verbs can't dispatch them). Link/unlink/
|
||||
// query/BFS/auto-link/stats all live in `edges.rs`, `bfs.rs`,
|
||||
// `auto_link.rs`.
|
||||
enabled_verbs: &[],
|
||||
fts_columns: None,
|
||||
// `None`: engine skips edge DDL. `cross_edges` is created via
|
||||
// `custom_migrations` with byte-identical legacy DDL.
|
||||
edge_table: None,
|
||||
// Documentation hint only (inert while `edge_table = None`).
|
||||
edge_key_kind: EdgeKeyKind::TextPair,
|
||||
archived_field: None,
|
||||
custom_migrations: &[DDL_CROSS_EDGES],
|
||||
};
|
||||
|
||||
/// Kept for backward compatibility with any external caller that
|
||||
/// imported `schema::create_schema` directly. New code should open via
|
||||
/// `Store::open` / `Store::open_memory`, which invokes the engine's
|
||||
/// migration runner with `CROSSDOMAIN_SCHEMA`.
|
||||
pub fn create_schema(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(DDL_CROSS_EDGES)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,38 @@
|
|||
use crate::schema::create_schema;
|
||||
use anyhow::{Context, Result};
|
||||
//! Crossdomain store — thin shim over `kei_entity_store::Store`.
|
||||
//!
|
||||
//! Layer-A convergence (2026-04-23): connection lifecycle + migrations +
|
||||
//! `PRAGMA user_version` stamping now ride the shared engine via
|
||||
//! `CROSSDOMAIN_SCHEMA`. Public surface preserved byte-for-byte so
|
||||
//! `edges.rs`, `bfs.rs`, `auto_link.rs`, and integration tests compile
|
||||
//! unchanged.
|
||||
//!
|
||||
//! Generic CRUD verbs are NOT wired here — kei-crossdomain is an
|
||||
//! edges-only store with bespoke TextPair+extras columns; see
|
||||
//! `schema.rs` for the architectural note on why engine's `link`/`rank`
|
||||
//! verbs cannot serve this crate without a destructive schema rewrite.
|
||||
|
||||
use crate::schema::CROSSDOMAIN_SCHEMA;
|
||||
use anyhow::Result;
|
||||
use kei_entity_store::Store as EntityStore;
|
||||
use rusqlite::Connection;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct Store {
|
||||
conn: Connection,
|
||||
inner: EntityStore,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let conn = Connection::open(path).context("open sqlite")?;
|
||||
conn.pragma_update(None, "journal_mode", "WAL").ok();
|
||||
create_schema(&conn)?;
|
||||
Ok(Self { conn })
|
||||
let inner = EntityStore::open(path, &CROSSDOMAIN_SCHEMA)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
pub fn open_memory() -> Result<Self> {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
create_schema(&conn)?;
|
||||
Ok(Self { conn })
|
||||
let inner = EntityStore::open_memory(&CROSSDOMAIN_SCHEMA)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
pub fn conn(&self) -> &Connection { &self.conn }
|
||||
pub fn conn(&self) -> &Connection {
|
||||
self.inner.conn()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue