using InvercargillSql.Migrations; using Sqlite; namespace ClankerSaddle.Data.Migrations { /** * Goal 27 refinements — the parking guard. `saddle suspend` on a * goal run that owns NO live task is refused by the web (there is * nothing whose terminal transition could ever wake the parked * agent): the park mechanics still run, but the run carries a * one-shot `pending_resume_notice`, so the daemon's next ask hands * it the refusal instruction instead of a sleep nothing ends. * * tasks.pending_resume_notice is that one-shot channel. It is set * by the refused park and consumed by the allocation that hands * the assignment out (the resume_notice discipline): while set, * a suspended task is immediately offerable — the exact lift the * task-level force_wake flag gives a plain parked task, but * carrying the guard's own instruction text. Nullable, NULL when * no refusal is pending. * * IDEMPOTENCE: same contract as M0002–M0006 — up() runs before the * apply transaction opens (the builder only queues), so it * introspects sqlite_master through a separate read-only handle * and adds the column only when the schema still lacks it. */ public class M0007_ParkingGuard : Migration { public override string name { get { return "Parking guard"; } } public override uint64 serial { get { return 7; } } public override string migration_namespace { get { return "clanker-saddle"; } } // Same resolution as M0006: the runner never hands up() a // connection, so the schema is read through a read-only handle // on the SPRY_DB_PATH main.vala connects to. private string db_path() { var path = Environment.get_variable("SPRY_DB_PATH") ?? ""; if (path.strip().length == 0) { return "./clanker-saddle.sqlite"; } return path; } private bool column_exists() { Database db; int rc = Database.open_v2(db_path(), out db, Sqlite.OPEN_READONLY); if (rc != Sqlite.OK) { return false; } Statement statement; rc = db.prepare_v2( "select 1 from pragma_table_info('tasks') where name = 'pending_resume_notice'", -1, out statement); if (rc != Sqlite.OK) { return false; } return statement.step() == Sqlite.ROW; } public override void up(MigrationBuilder b) { if (column_exists()) { return; } b.alter_table("tasks", (t) => { t.add_column("pending_resume_notice"); }); } public override void down(MigrationBuilder b) { // SQLite cannot drop a column through the builder's // alter_table path; the raw statement is the honest form. b.execute_sql("alter table tasks drop column pending_resume_notice"); } } }