workspace: canonicalize legacy workspaces
This commit is contained in:
parent
5149ab703f
commit
d89ace5b9c
|
|
@ -517,4 +517,4 @@ If implementation is included in this Ticket, prefer a small non-breaking migrat
|
||||||
|
|
||||||
The `yoi-workspace-server` SQLite bootstrap migration implements this v0 schema as schema version 2. Fresh databases create the typed tables listed above and deliberately do not create canonical `runs`, `hosts`, `workers`, `actors`, or check/validation result tables. Host and Worker HTTP read APIs remain live runtime views backed by local inspection, not DB tables.
|
The `yoi-workspace-server` SQLite bootstrap migration implements this v0 schema as schema version 2. Fresh databases create the typed tables listed above and deliberately do not create canonical `runs`, `hosts`, `workers`, `actors`, or check/validation result tables. Host and Worker HTTP read APIs remain live runtime views backed by local inspection, not DB tables.
|
||||||
|
|
||||||
For databases created by the earlier workspace-server bootstrap, migration version 2 preserves old `repositories`, `runs`, `artifacts`, `ticket_projections`, and `objective_projections` data by renaming those tables to `legacy_repositories`, `legacy_runs`, `legacy_artifacts`, `legacy_ticket_projections`, and `legacy_objective_projections`, then creating the v0 typed tables. The legacy names are compatibility preservation only and are not canonical schema tables or active write authority.
|
For databases created by the earlier workspace-server bootstrap, migration version 2 preserves old `workspaces`, `repositories`, `runs`, `artifacts`, `ticket_projections`, and `objective_projections` data by renaming those tables to `legacy_workspaces`, `legacy_repositories`, `legacy_runs`, `legacy_artifacts`, `legacy_ticket_projections`, and `legacy_objective_projections`, then creating the v0 typed tables. Existing legacy workspace rows are copied into the canonical v0 `workspaces` table with `state = active` when the old row had no typed state. The legacy names are compatibility preservation only and are not canonical schema tables or active write authority.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
|
|
||||||
|
const WORKSPACES_V0_COLUMNS: &[&str] = &[
|
||||||
|
"workspace_id",
|
||||||
|
"display_name",
|
||||||
|
"state",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
];
|
||||||
|
|
||||||
const MIGRATIONS: &[Migration] = &[
|
const MIGRATIONS: &[Migration] = &[
|
||||||
Migration {
|
Migration {
|
||||||
version: 1,
|
version: 1,
|
||||||
|
|
@ -195,12 +203,54 @@ fn align_legacy_bootstrap_schema(conn: &Connection) -> Result<()> {
|
||||||
"legacy_objective_projections",
|
"legacy_objective_projections",
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
if table_exists(conn, "workspaces")? && !column_exists(conn, "workspaces", "state")? {
|
|
||||||
conn.execute_batch(
|
let legacy_workspaces = preserve_noncanonical_workspaces(conn)?;
|
||||||
"ALTER TABLE workspaces ADD COLUMN state TEXT NOT NULL DEFAULT 'active';",
|
create_schema_v0_tables(conn)?;
|
||||||
)?;
|
if let Some(legacy_table) = legacy_workspaces {
|
||||||
|
copy_legacy_workspaces(conn, &legacy_table)?;
|
||||||
}
|
}
|
||||||
create_schema_v0_tables(conn)
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preserve_noncanonical_workspaces(conn: &Connection) -> Result<Option<String>> {
|
||||||
|
if !table_exists(conn, "workspaces")? {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let columns = table_columns(conn, "workspaces")?;
|
||||||
|
if columns
|
||||||
|
.iter()
|
||||||
|
.map(String::as_str)
|
||||||
|
.eq(WORKSPACES_V0_COLUMNS.iter().copied())
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let legacy_table = "legacy_workspaces";
|
||||||
|
rename_legacy_table(conn, "workspaces", legacy_table)?;
|
||||||
|
Ok(Some(legacy_table.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_legacy_workspaces(conn: &Connection, legacy_table: &str) -> Result<()> {
|
||||||
|
let columns = table_columns(conn, legacy_table)?;
|
||||||
|
for required_column in ["workspace_id", "display_name", "created_at", "updated_at"] {
|
||||||
|
if !columns.iter().any(|column| column == required_column) {
|
||||||
|
return Err(Error::Store(format!(
|
||||||
|
"cannot migrate legacy workspaces: `{legacy_table}` is missing `{required_column}`"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let state_expr = if columns.iter().any(|column| column == "state") {
|
||||||
|
"COALESCE(NULLIF(state, ''), 'active')"
|
||||||
|
} else {
|
||||||
|
"'active'"
|
||||||
|
};
|
||||||
|
conn.execute_batch(&format!(
|
||||||
|
r#"INSERT OR IGNORE INTO workspaces (
|
||||||
|
workspace_id, display_name, state, created_at, updated_at
|
||||||
|
)
|
||||||
|
SELECT workspace_id, display_name, {state_expr}, created_at, updated_at
|
||||||
|
FROM {legacy_table};"#
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rename_legacy_table(conn: &Connection, table_name: &str, legacy_name: &str) -> Result<()> {
|
fn rename_legacy_table(conn: &Connection, table_name: &str, legacy_name: &str) -> Result<()> {
|
||||||
|
|
@ -617,6 +667,20 @@ mod tests {
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
configure_sqlite(&conn).unwrap();
|
configure_sqlite(&conn).unwrap();
|
||||||
conn.execute_batch(LEGACY_BOOTSTRAP_SQL).unwrap();
|
conn.execute_batch(LEGACY_BOOTSTRAP_SQL).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
r#"INSERT INTO workspaces (
|
||||||
|
workspace_id, display_name, local_root, record_authority, created_at, updated_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
|
||||||
|
params![
|
||||||
|
"legacy-workspace",
|
||||||
|
"Legacy Workspace",
|
||||||
|
"/tmp/legacy-workspace",
|
||||||
|
"local_yoi_project_records",
|
||||||
|
"2026-01-01T00:00:00Z",
|
||||||
|
"2026-01-02T00:00:00Z",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO __yoi_schema_migrations (version, name) VALUES (1, 'bootstrap workspace control plane')",
|
"INSERT INTO __yoi_schema_migrations (version, name) VALUES (1, 'bootstrap workspace control plane')",
|
||||||
[],
|
[],
|
||||||
|
|
@ -637,6 +701,7 @@ mod tests {
|
||||||
"ticket_worker_links",
|
"ticket_worker_links",
|
||||||
"artifacts",
|
"artifacts",
|
||||||
"audit_events",
|
"audit_events",
|
||||||
|
"legacy_workspaces",
|
||||||
"legacy_repositories",
|
"legacy_repositories",
|
||||||
"legacy_runs",
|
"legacy_runs",
|
||||||
"legacy_artifacts",
|
"legacy_artifacts",
|
||||||
|
|
@ -654,14 +719,58 @@ mod tests {
|
||||||
"upgraded schema must not retain forbidden canonical table {forbidden}"
|
"upgraded schema must not retain forbidden canonical table {forbidden}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let workspace_columns = table_columns(conn, "workspaces")?;
|
assert_columns(
|
||||||
assert!(workspace_columns.iter().any(|column| column == "state"));
|
conn,
|
||||||
|
"workspaces",
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"display_name",
|
||||||
|
"state",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let legacy_workspace_columns = table_columns(conn, "legacy_workspaces")?;
|
||||||
|
assert!(
|
||||||
|
legacy_workspace_columns
|
||||||
|
.iter()
|
||||||
|
.any(|column| column == "local_root")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
legacy_workspace_columns
|
||||||
|
.iter()
|
||||||
|
.any(|column| column == "record_authority")
|
||||||
|
);
|
||||||
let artifact_columns = table_columns(conn, "artifacts")?;
|
let artifact_columns = table_columns(conn, "artifacts")?;
|
||||||
assert!(artifact_columns.iter().any(|column| column == "uri"));
|
assert!(artifact_columns.iter().any(|column| column == "uri"));
|
||||||
assert!(!artifact_columns.iter().any(|column| column == "run_id"));
|
assert!(!artifact_columns.iter().any(|column| column == "run_id"));
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
store.get_workspace("legacy-workspace").await.unwrap(),
|
||||||
|
Some(WorkspaceRecord {
|
||||||
|
workspace_id: "legacy-workspace".to_string(),
|
||||||
|
display_name: "Legacy Workspace".to_string(),
|
||||||
|
state: "active".to_string(),
|
||||||
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_record = WorkspaceRecord {
|
||||||
|
workspace_id: "new-workspace".to_string(),
|
||||||
|
display_name: "New Workspace".to_string(),
|
||||||
|
state: "active".to_string(),
|
||||||
|
created_at: "2026-02-01T00:00:00Z".to_string(),
|
||||||
|
updated_at: "2026-02-01T00:00:00Z".to_string(),
|
||||||
|
};
|
||||||
|
store.upsert_workspace(&new_record).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
store.get_workspace("new-workspace").await.unwrap(),
|
||||||
|
Some(new_record)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn table_names(conn: &Connection) -> BTreeSet<String> {
|
fn table_names(conn: &Connection) -> BTreeSet<String> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user