oc_chat
Loading...
Searching...
No Matches
v0_3_2_roadmap_source_fields.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/migrations/v0_3_2_roadmap_source_fields.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_schema_v0_3_2.py"
6# OCT-LINT: disable=no_hardcoded_secrets reason="Schema object names are migration constants, not secrets"
7"""
8Runtime migration for roadmap source fields v0.3.2.
9
10Purpose
11-------
12Apply the additive schema changes that V0.22 §0 [OP-1] confirmed for the
13roadmap source-of-truth cutover (Plan 142): ``purpose`` columns on
14``roadmap_phases`` and ``roadmap_items_v3``; ``successor_item_id`` on
15``roadmap_items_v3``; retirement of the ``roadmap_exec_order``/``roadmap_todo``
16``file_kind``s via a SQLite table-rebuild of ``roadmap_render_sections``; and
17the base-capability ``registry_meta.store_scope`` column.
18
19Spec: ``REGISTRY_ROADMAP_SCHEMA_V0_3_1.md`` (L-a), §1–§6, §9.3.
20
21Responsibilities
22----------------
23- Add ``purpose TEXT NOT NULL DEFAULT ''`` to ``roadmap_phases`` and
24 ``roadmap_items_v3`` (§2).
25- Add ``successor_item_id TEXT`` self-FK to ``roadmap_items_v3`` (§3).
26- Rebuild ``roadmap_render_sections`` with the six-token ``file_kind`` CHECK
27 and deactivate retired ``file_kind`` rows (§5).
28- Add ``registry_meta.store_scope`` base-capability column (§9.3).
29- Bump SQLite ``user_version`` to 32.
30
31Diagnostics
32-----------
33Domain: OC_CHAT.MAIN
34Levels:
35 L2 - migration lifecycle
36 L3 - schema version decisions
37 L4 - table definition checks
38
39Contracts
40---------
41- Callers own transaction boundaries.
42- The migration is idempotent (guarded by ``is_applied``).
43- ``rollback`` reverts bookkeeping only; table shape is backward-compatible.
44
45Dependencies
46------------
47- sqlite3 (migration connection)
48- oc_diagnostics (_dbg structured logger)
49"""
50from __future__ import annotations
51
52import sqlite3
53
54from oc_diagnostics import _dbg
55
56_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
57
58ROADMAP_SOURCE_FIELDS_SCHEMA_VERSION = 32
59ROADMAP_SOURCE_FIELDS_SCHEMA_DESCRIPTION = (
60 # OCT-LINT: disable=no_hardcoded_secrets reason="Schema object names are migration constants, not secrets"
61 "roadmap source fields REGISTRY_ROADMAP_SCHEMA_V0_3_1"
62)
63
64# Probe strings used by is_applied (§5 / §9.3).
65# OCT-LINT: disable=no_hardcoded_secrets reason="Schema object names are migration constants, not secrets"
66_ROADMAP_EXTENDED_TOKEN = "'roadmap_extended'"
67_STORE_SCOPE_COLUMN = "store_scope"
68
69
70def current_user_version(conn: sqlite3.Connection) -> int:
71 """Return SQLite's integer schema user version."""
72
73 return int(conn.execute("PRAGMA user_version").fetchone()[0])
74
75
76def apply(conn: sqlite3.Connection) -> None:
77 """Apply the v0.3.2 roadmap source fields migration if needed."""
78
79 if is_applied(conn):
80 conn.execute(
81 f"PRAGMA user_version = {ROADMAP_SOURCE_FIELDS_SCHEMA_VERSION}"
82 )
83 return
84 _run_migration(conn)
85 conn.execute(
86 f"PRAGMA user_version = {ROADMAP_SOURCE_FIELDS_SCHEMA_VERSION}"
87 )
88
89
90def rollback(conn: sqlite3.Connection) -> None:
91 """Reset user_version for tests; table shape remains backward-compatible.
92
93 Mirrors ``v0_3_1_auth_events.rollback``: revert bookkeeping only.
94 """
95
96 conn.execute("PRAGMA user_version = 31")
97 conn.execute(
98 "DELETE FROM schema_migrations WHERE version = ?",
99 (ROADMAP_SOURCE_FIELDS_SCHEMA_VERSION,),
100 )
101
102
103def is_applied(conn: sqlite3.Connection) -> bool:
104 """Return True when all four migration effects are present (L-a §5 / §9.3).
105
106 Probe 1 — ``roadmap_phases.purpose`` AND ``roadmap_items_v3.purpose`` exist.
107 Probe 2 — ``roadmap_items_v3.successor_item_id`` exists.
108 Probe 3 — ``roadmap_render_sections.file_kind`` CHECK contains ``'roadmap_extended'``.
109 Probe 4 — ``registry_meta.store_scope`` exists.
110 """
111
112 if not _has_column(conn, "roadmap_phases", "purpose"):
113 return False
114 items_cols = _column_names(conn, "roadmap_items_v3")
115 if "purpose" not in items_cols or "successor_item_id" not in items_cols:
116 return False
117 row = conn.execute(
118 # OCT-LINT: disable=no_hardcoded_secrets reason="Schema object names are migration constants, not secrets"
119 "SELECT sql FROM sqlite_master"
120 " WHERE type = 'table' AND name = 'roadmap_render_sections'"
121 ).fetchone()
122 if row is None:
123 return False
124 try:
125 sql = row["sql"]
126 except (TypeError, KeyError, IndexError):
127 sql = row[0]
128 if _ROADMAP_EXTENDED_TOKEN not in str(sql):
129 return False
130 return _has_column(conn, "registry_meta", _STORE_SCOPE_COLUMN)
131
132
133def migration_sql() -> str:
134 """Return SQL used when building a fresh schema in one executescript call.
135
136 This string is appended after ``v0_3_1_auth_events.migration_sql()`` in
137 ``_run_schema_script`` inside an outer ``BEGIN IMMEDIATE`` transaction.
138 No inner BEGIN/COMMIT — the PRAGMA foreign_keys lines are no-ops inside a
139 transaction but take effect when ``apply()`` calls executescript in
140 autocommit mode.
141 """
142
143 return _MIGRATION_SQL + f"\nPRAGMA user_version = {ROADMAP_SOURCE_FIELDS_SCHEMA_VERSION};\n"
144
145
146def _run_migration(conn: sqlite3.Connection) -> None:
147 conn.executescript(_MIGRATION_SQL)
148
149
150def _column_names(conn: sqlite3.Connection, table: str) -> set[str]:
151 rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
152 result: set[str] = set()
153 for row in rows:
154 try:
155 result.add(str(row["name"]))
156 except (TypeError, KeyError, IndexError):
157 result.add(str(row[1]))
158 return result
159
160
161def _has_column(conn: sqlite3.Connection, table: str, column: str) -> bool:
162 return column in _column_names(conn, table)
163
164
165# ---------------------------------------------------------------------------
166# Migration SQL (no transaction wrappers — see migration_sql() docstring).
167# ---------------------------------------------------------------------------
168
169# OCT-LINT: disable=no_hardcoded_secrets reason="Schema object names are migration constants, not secrets"
170_MIGRATION_SQL = """
171ALTER TABLE roadmap_phases ADD COLUMN purpose TEXT NOT NULL DEFAULT '';
172ALTER TABLE roadmap_items_v3 ADD COLUMN purpose TEXT NOT NULL DEFAULT '';
173ALTER TABLE roadmap_items_v3 ADD COLUMN successor_item_id TEXT REFERENCES roadmap_items_v3(item_id);
174
175PRAGMA foreign_keys = OFF;
176
177CREATE TABLE roadmap_render_sections_new (
178 id INTEGER PRIMARY KEY AUTOINCREMENT,
179 file_kind TEXT NOT NULL
180 CHECK(file_kind IN (
181 'roadmap','roadmap_exec_order','roadmap_todo','roadmap_changelog',
182 'roadmap_extended','root_changelog'
183 )),
184 section_path TEXT NOT NULL,
185 sort_order INTEGER NOT NULL,
186 query_kind TEXT NOT NULL
187 CHECK(query_kind IN (
188 'm_phase_table',
189 'phase_section',
190 'phase_section_with_subrows',
191 'backlog_track_table',
192 'git_workflow_table',
193 'completed_in_version',
194 'recently_completed_window',
195 'prose_only'
196 )),
197 query_params_json TEXT,
198 required_min_version TEXT REFERENCES roadmap_versions(version_id),
199 active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1))
200);
201
202INSERT INTO roadmap_render_sections_new
203 (id, file_kind, section_path, sort_order, query_kind,
204 query_params_json, required_min_version, active)
205SELECT
206 id, file_kind, section_path, sort_order, query_kind,
207 query_params_json, required_min_version, active
208FROM roadmap_render_sections;
209
210DROP TABLE roadmap_render_sections;
211
212ALTER TABLE roadmap_render_sections_new RENAME TO roadmap_render_sections;
213
214CREATE INDEX idx_roadmap_render_sections_file
215 ON roadmap_render_sections(file_kind, sort_order);
216
217UPDATE roadmap_render_sections
218 SET active = 0
219 WHERE file_kind IN ('roadmap_exec_order', 'roadmap_todo');
220
221PRAGMA foreign_key_check;
222
223PRAGMA foreign_keys = ON;
224
225ALTER TABLE registry_meta
226 ADD COLUMN store_scope TEXT NOT NULL DEFAULT 'live_store'
227 CHECK(store_scope IN ('live_store', 'authored_projection'));
228"""
bool _has_column(sqlite3.Connection conn, str table, str column)