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.
|
||||
|
||||
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};
|
||||
|
||||
const WORKSPACES_V0_COLUMNS: &[&str] = &[
|
||||
"workspace_id",
|
||||
"display_name",
|
||||
"state",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
];
|
||||
|
||||
const MIGRATIONS: &[Migration] = &[
|
||||
Migration {
|
||||
version: 1,
|
||||
|
|
@ -195,12 +203,54 @@ fn align_legacy_bootstrap_schema(conn: &Connection) -> Result<()> {
|
|||
"legacy_objective_projections",
|
||||
)?;
|
||||
}
|
||||
if table_exists(conn, "workspaces")? && !column_exists(conn, "workspaces", "state")? {
|
||||
conn.execute_batch(
|
||||
"ALTER TABLE workspaces ADD COLUMN state TEXT NOT NULL DEFAULT 'active';",
|
||||
)?;
|
||||
|
||||
let legacy_workspaces = preserve_noncanonical_workspaces(conn)?;
|
||||
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<()> {
|
||||
|
|
@ -617,6 +667,20 @@ mod tests {
|
|||
let conn = Connection::open_in_memory().unwrap();
|
||||
configure_sqlite(&conn).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(
|
||||
"INSERT INTO __yoi_schema_migrations (version, name) VALUES (1, 'bootstrap workspace control plane')",
|
||||
[],
|
||||
|
|
@ -637,6 +701,7 @@ mod tests {
|
|||
"ticket_worker_links",
|
||||
"artifacts",
|
||||
"audit_events",
|
||||
"legacy_workspaces",
|
||||
"legacy_repositories",
|
||||
"legacy_runs",
|
||||
"legacy_artifacts",
|
||||
|
|
@ -654,14 +719,58 @@ mod tests {
|
|||
"upgraded schema must not retain forbidden canonical table {forbidden}"
|
||||
);
|
||||
}
|
||||
let workspace_columns = table_columns(conn, "workspaces")?;
|
||||
assert!(workspace_columns.iter().any(|column| column == "state"));
|
||||
assert_columns(
|
||||
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")?;
|
||||
assert!(artifact_columns.iter().any(|column| column == "uri"));
|
||||
assert!(!artifact_columns.iter().any(|column| column == "run_id"));
|
||||
Ok(())
|
||||
})
|
||||
.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> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user